Кэширование операций распаковки защищённых данных становится
критически важным при работе с механизмом seal/unseal,
который используется в экосистеме Iron для безопасного хранения и
передачи структурированных данных.
Процедура unseal в Iron представляет собой обратную
операцию к seal. Она включает:
T_{unseal} = T_{verify} + T_{decrypt} + T_{deserialize}
Каждый вызов 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;
}
Такой подход даёт мгновенный выигрыш в сценариях повторного использования токенов.
Несмотря на эффективность, прямолинейная реализация имеет несколько проблем:
Поэтому требуется более строгая модель хранения.
Сама концепция 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;
}
Одинаковая строка sealed может давать разные результаты
при разных конфигурациях:
Игнорирование этого приводит к некорректному кэшированию.
Правильный ключ должен включать все значимые параметры:
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');
}
При высоконагруженных системах 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;
При проектировании системы важно учитывать:
В высоконагруженных приложениях оптимальная архитектура выглядит как комбинация: