IndexedDB используется как клиентское хранилище для структурированных данных и позволяет сохранять большие объёмы информации, включая бинарные данные. В связке с криптографической библиотекой SJCL (Stanford JavaScript Crypto Library) IndexedDB становится основой для построения локальных защищённых хранилищ, где данные сохраняются уже в зашифрованном виде.
Ключевая идея архитектуры заключается в том, что IndexedDB никогда не работает с открытым текстом. Все операции записи выполняются с уже зашифрованными структурами, а расшифровка происходит только в оперативной памяти после чтения.
Типичная запись в IndexedDB при использовании SJCL включает следующие компоненты:
id — идентификатор записиciphertext — зашифрованные данные (строка SJCL)salt — соль для ключа (если используется парольная
деривация)iv — вектор инициализации (если вынесен отдельно)iter — количество итераций KDFtag — аутентификационный тег (для GCM/CCM режимов)meta — незашифрованные метаданные (опционально)Важно разделять:
Структура базы обычно минимальна, чтобы избежать утечек через схему хранения.
const request = indexedDB.open("secureVault", 1);
request.onupgradenee ded = function (event) {
const db = event.target.result;
const store = db.createObjectStore("records", {
keyPath: "id"
});
store.createIndex("updated", "updated", { unique: false });
};
Object store не содержит сложной структуры — вся криптография происходит вне базы.
SJCL предоставляет высокоуровневый API для симметричного шифрования:
const plaintext = JSON.stringify({
login: "user123",
password: "secret_password",
notes: "важные данные"
});
const password = "master_password";
const encrypted = sjcl.encrypt(password, plaintext);
Результат sjcl.encrypt — JSON-строка, содержащая:
Эта строка уже готова к хранению в IndexedDB без дополнительной обработки.
После шифрования данные помещаются в транзакцию записи:
function saveEncryptedRecord(db, id, encryptedData) {
const tx = db.transaction(["records"], "readwrite");
const store = tx.objectStore("records");
const record = {
id: id,
ciphertext: encryptedData,
updated: Date.now()
};
store.put(record);
}
Важный момент: IndexedDB не имеет представления о содержимом ciphertext. Для него это просто строка.
При извлечении записи происходит обратная операция: получение ciphertext и расшифровка через SJCL.
function getDecryptedRecord(db, id, password) {
return new Promise((resolve, reject) => {
const tx = db.transaction(["records"], "readonly");
const store = tx.objectStore("records");
const request = store.get(id);
request.onsucc ess = function () {
const record = request.result;
try {
const decrypted = sjcl.decrypt(password, record.ciphertext);
resolve(JSON.parse(decrypted));
} catch (e) {
reject("Ошибка расшифровки");
}
};
request.oner ror = function () {
reject("Ошибка чтения IndexedDB");
};
});
}
SJCL автоматически извлекает salt, iv и параметры KDF из структуры JSON.
SJCL использует PBKDF2 для преобразования пароля в криптографический ключ. Основные параметры:
salt — случайное значениеiter — количество итерацийkeySize — размер ключаПример явного управления параметрами:
const encrypted = sjcl.encrypt(password, plaintext, {
iter: 10000,
ks: 256,
mode: "ccm"
});
Чем выше iter, тем выше устойчивость к перебору, но ниже
производительность.
IndexedDB поддерживает Blob и ArrayBuffer, однако SJCL по умолчанию работает со строками. Для бинарных данных используется предварительное преобразование:
function arrayBufferToString(buffer) {
return String.fromCharCode.apply(null, new Uint8Array(buffer));
}
function stringToArrayBuffer(str) {
const buf = new ArrayBuffer(str.length);
const view = new Uint8Array(buf);
for (let i = 0; i < str.length; i++) {
view[i] = str.charCodeAt(i);
}
return buf;
}
После преобразования бинарные данные могут быть зашифрованы SJCL и сохранены как строка.
Обновление записи в IndexedDB при криптографическом подходе всегда означает полную перезапись ciphertext:
function updateRecord(db, id, newPlainData, password) {
const encrypted = sjcl.encrypt(password, JSON.stringify(newPlainData));
const tx = db.transaction(["records"], "readwrite");
const store = tx.objectStore("records");
store.put({
id: id,
ciphertext: encrypted,
updated: Date.now()
});
}
Частичные изменения внутри зашифрованного блока невозможны без дешифрования.
При объединении SJCL и IndexedDB формируется модель:
Это исключает возможность прямого извлечения данных из IndexedDB даже при компрометации хранилища браузера.
Ошибки SJCL при расшифровке часто связаны с:
Типичная обработка:
try {
const data = sjcl.decrypt(password, ciphertext);
} catch (e) {
if (e.message.includes("ccm: tag doesn't match")) {
// нарушение целостности данных
}
}
IndexedDB гарантирует атомарность транзакций, но не целостность криптографического уровня. Поэтому целесообразно:
Пример поля версии:
{
id: "1",
ciphertext: "...",
cryptoVersion: 1
}
При построении более сложных систем IndexedDB используется как слой хранения, а SJCL как слой защиты:
Такое разделение позволяет изолировать криптографию от логики доступа к данным.
IndexedDB не может индексировать содержимое ciphertext, поэтому:
Пример:
store.createIndex("updated", "updated");
store.createIndex("type", "meta.type");
Если требуется поиск по содержимому, используется либо:
Иногда требуется поиск без раскрытия данных:
const hash = sjcl.hash.sha256.hash("login:user123");
Хэш сохраняется отдельно:
{
id: "1",
ciphertext: "...",
lookupHash: sjcl.codec.hex.fromBits(hash)
}
Это позволяет выполнять фильтрацию без дешифровки всех записей.
Основные узкие места:
Оптимизация обычно достигается через:
При работе с SJCL и IndexedDB критично учитывать:
Любое промежуточное состояние между decrypt и render считается потенциально уязвимым и должно быть краткоживущим в памяти.