Работа с IndexedDB и зашифрованными записями

IndexedDB используется как клиентское хранилище для структурированных данных и позволяет сохранять большие объёмы информации, включая бинарные данные. В связке с криптографической библиотекой SJCL (Stanford JavaScript Crypto Library) IndexedDB становится основой для построения локальных защищённых хранилищ, где данные сохраняются уже в зашифрованном виде.

Ключевая идея архитектуры заключается в том, что IndexedDB никогда не работает с открытым текстом. Все операции записи выполняются с уже зашифрованными структурами, а расшифровка происходит только в оперативной памяти после чтения.


Базовая модель данных зашифрованной записи

Типичная запись в IndexedDB при использовании SJCL включает следующие компоненты:

  • id — идентификатор записи
  • ciphertext — зашифрованные данные (строка SJCL)
  • salt — соль для ключа (если используется парольная деривация)
  • iv — вектор инициализации (если вынесен отдельно)
  • iter — количество итераций KDF
  • tag — аутентификационный тег (для GCM/CCM режимов)
  • meta — незашифрованные метаданные (опционально)

Важно разделять:

  • шифротекст (ciphertext) — защищённая часть
  • метаданные — минимальная информация, не раскрывающая содержимое

Инициализация IndexedDB для криптографического хранилища

Структура базы обычно минимальна, чтобы избежать утечек через схему хранения.

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 перед записью

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-строка, содержащая:

  • ciphertext
  • salt
  • iv
  • iter
  • mode
  • adata (если используется)

Эта строка уже готова к хранению в IndexedDB без дополнительной обработки.


Запись зашифрованного объекта в 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 формируется модель:

  1. Данные шифруются в памяти
  2. В IndexedDB попадает только ciphertext
  3. Пароль никогда не сохраняется в хранилище
  4. Расшифровка выполняется только при чтении

Это исключает возможность прямого извлечения данных из IndexedDB даже при компрометации хранилища браузера.


Обработка ошибок криптографического уровня

Ошибки SJCL при расшифровке часто связаны с:

  • неверным паролем
  • повреждённым ciphertext
  • изменёнными параметрами KDF
  • обрезанными JSON структурами

Типичная обработка:

try {
    const data = sjcl.decrypt(password, ciphertext);
} catch (e) {
    if (e.message.includes("ccm: tag doesn't match")) {
        // нарушение целостности данных
    }
}

Согласованность данных между транзакциями

IndexedDB гарантирует атомарность транзакций, но не целостность криптографического уровня. Поэтому целесообразно:

  • хранить версию схемы шифрования
  • фиксировать параметры SJCL в каждой записи
  • избегать смешивания разных режимов шифрования в одном store

Пример поля версии:

{
    id: "1",
    ciphertext: "...",
    cryptoVersion: 1
}

Паттерн «шифрованный репозиторий»

При построении более сложных систем IndexedDB используется как слой хранения, а SJCL как слой защиты:

  • Repository слой управляет CRUD операциями
  • Crypto слой отвечает за encrypt/decrypt
  • Storage слой (IndexedDB) только сохраняет blob-данные

Такое разделение позволяет изолировать криптографию от логики доступа к данным.


Индексация зашифрованных данных

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)
}

Это позволяет выполнять фильтрацию без дешифровки всех записей.


Особенности производительности

Основные узкие места:

  • PBKDF2 при высоких итерациях
  • сериализация JSON перед шифрованием
  • преобразование строк/байтов
  • транзакции IndexedDB при массовых операциях

Оптимизация обычно достигается через:

  • батчевую запись
  • кеширование ключей в памяти
  • минимизацию пересборки ciphertext

Безопасность жизненного цикла данных

При работе с SJCL и IndexedDB критично учитывать:

  • очистку памяти после использования ключей
  • отсутствие хранения plaintext в промежуточных структурах
  • исключение логирования ciphertext в консоль
  • контроль версий алгоритмов шифрования

Любое промежуточное состояние между decrypt и render считается потенциально уязвимым и должно быть краткоживущим в памяти.