Кэширование результатов unseal

Кэширование операций распаковки защищённых данных становится критически важным при работе с механизмом seal/unseal, который используется в экосистеме Iron для безопасного хранения и передачи структурированных данных.

Процедура unseal в Iron представляет собой обратную операцию к seal. Она включает:

  • проверку целостности подписи
  • расшифровку данных
  • валидацию параметров (время жизни, алгоритмы, ключи)
  • десериализацию исходного объекта

T_{unseal} = T_{verify} + T_{decrypt} + T_{deserialize}

Каждый вызов unseal не является дешёвой операцией. Даже при использовании быстрых криптографических алгоритмов основная стоимость формируется за счёт:

  • вычисления HMAC или аналогов подписи
  • симметричного шифрования/дешифрования
  • обработки сериализованных структур

При высокой частоте запросов это превращается в узкое место производительности.

Причины повторных вызовов unseal

В реальных приложениях одна и та же зашифрованная строка часто декодируется многократно:

  • идентичные cookie в рамках одного запроса и цепочки middleware
  • повторное чтение сессии в разных слоях приложения
  • несколько сервисов внутри одного процесса используют один и тот же токен
  • повторные обращения к одному и тому же payload в течение короткого времени

Это создаёт ситуацию, в которой вычисления повторяются без необходимости.

Базовая стратегия кэширования результата unseal

Наиболее прямолинейный подход — in-memory кэш с ключом, основанным на входной строке.

Ключевой принцип: результат unseal детерминирован от входных данных и ключей.

import Iron from '@hapi/iron';

const cache = new Map();

async function cachedUnseal(sealed, password, options) {
    const cacheKey = sealed;

    if (cache.has(cacheKey)) {
        return cache.get(cacheKey);
    }

    const result = await Iron.unseal(sealed, password, options);
    cache.set(cacheKey, result);

    return result;
}

Такой подход даёт мгновенный выигрыш в сценариях повторного использования токенов.

Ограничения простого Map-кэша

Несмотря на эффективность, прямолинейная реализация имеет несколько проблем:

  • бесконтрольный рост памяти
  • отсутствие учёта TTL данных
  • невозможность учитывать разные ключи/опции при одинаковом payload
  • риск хранения устаревших сессий

Поэтому требуется более строгая модель хранения.

Кэширование с учётом времени жизни

Сама концепция Iron предполагает наличие временных ограничений внутри зашифрованного объекта. Кэш должен учитывать это.

T_{cache} = (T_{unseal}, T_{ttl})

Практическая реализация включает хранение метки времени:

const cache = new Map();

function setCache(key, value, ttlMs) {
    const expiresAt = Date.now() + ttlMs;

    cache.set(key, {
        value,
        expiresAt
    });
}

function getCache(key) {
    const entry = cache.get(key);

    if (!entry) return null;

    if (Date.now() > entry.expiresAt) {
        cache.delete(key);
        return null;
    }

    return entry.value;
}

Интеграция с unseal:

async function cachedUnseal(sealed, password, options) {
    const cached = getCache(sealed);

    if (cached) {
        return cached;
    }

    const result = await Iron.unseal(sealed, password, options);

    setCache(sealed, result, 60_000);

    return result;
}

Учёт параметров Iron при формировании ключа

Одинаковая строка sealed может давать разные результаты при разных конфигурациях:

  • различные passwords
  • разные алгоритмы
  • разные настройки Iron (salt, integrity checks)

Игнорирование этого приводит к некорректному кэшированию.

Правильный ключ должен включать все значимые параметры:

function buildCacheKey(sealed, password, options) {
    return JSON.stringify({
        sealed,
        password,
        options
    });
}

Более производительный вариант — хеширование:

import crypto from 'crypto';

function buildCacheKey(sealed, password, options) {
    return crypto
        .createHash('sha256')
        .update(sealed + password + JSON.stringify(options))
        .digest('hex');
}

LRU-кэш для ограничения памяти

При высоконагруженных системах Map-кэш становится неприемлемым без ограничений. Используется LRU-стратегия.

import LRU from 'lru-cache';

const cache = new LRU({
    max: 5000,
    ttl: 1000 * 60
});

Интеграция:

async function cachedUnseal(sealed, password, options) {
    const key = buildCacheKey(sealed, password, options);

    const cached = cache.get(key);
    if (cached) return cached;

    const result = await Iron.unseal(sealed, password, options);

    cache.set(key, result);

    return result;
}

Конкурентные вызовы и защита от дублирования вычислений

В реальных системах несколько параллельных запросов могут одновременно инициировать unseal для одного и того же значения. Это создаёт эффект «cache stampede».

Решение — хранение Promise вместо результата:

const cache = new Map();

async function cachedUnseal(sealed, password, options) {
    const key = buildCacheKey(sealed, password, options);

    if (cache.has(key)) {
        return cache.get(key);
    }

    const promise = Iron.unseal(sealed, password, options)
        .then(result => {
            cache.set(key, result);
            return result;
        })
        .catch(err => {
            cache.delete(key);
            throw err;
        });

    cache.set(key, promise);

    return promise;
}

Такой подход гарантирует, что криптографическая операция выполняется ровно один раз на уникальный вход.

Инвалидация кэша при смене ключей

Смена secret key полностью меняет семантику unseal. Старые данные становятся недействительными.

Поэтому при ротации ключей кэш должен очищаться:

function rotateKey(newPassword) {
    password = newPassword;
    cache.clear();
}

В более сложных системах применяется namespace-изоляция:

const cacheNamespace = passwordVersion;

Производственные рекомендации по использованию кэширования unseal

При проектировании системы важно учитывать:

  • кэшировать только после успешной валидации подписи
  • не увеличивать TTL выше срока жизни токена
  • учитывать риск утечки данных через память процесса
  • избегать глобальных кэшей в многопользовательских средах без изоляции

В высоконагруженных приложениях оптимальная архитектура выглядит как комбинация:

  • LRU-кэш в памяти процесса
  • ограниченный TTL
  • ключ, зависящий от всех криптографических параметров
  • защита от параллельных вычислений через Promise-дедупликацию