В основе механизма Iron (в экосистеме @hapi/iron) лежит
концепция «запечатывания» данных с помощью симметричного секрета. На
практике используется не один ключ, а массив ключей, где:
При ротации ключей важно обеспечить два свойства:
Любое тестирование ротации ключей должно проверять именно эту двойственность поведения.
Типичный вариант настройки выглядит так:
import Iron from '@hapi/iron';
const keys = [
'current-secret-key',
'old-secret-key-1',
'old-secret-key-2'
];
const options = {
encryption: {
saltBits: 256,
algorithm: 'aes-256-cbc'
}
};
В этом наборе:
keys[0] — активный ключТестирование начинается с проверки того, что сериализация использует только активный ключ.
import Iron from '@hapi/iron';
test('шифрование использует только первый ключ', async () => {
const keys = ['key-A', 'key-B'];
const sealed = await Iron.seal(
{ user: 'alice' },
keys[0],
Iron.defaults
);
expect(typeof sealed).toBe('string');
});
Важный момент: в Iron нельзя напрямую указать массив ключей при
seal. Ротация проявляется на этапе unseal.
Основная цель тестов — убедиться, что данные, зашифрованные старым ключом, всё ещё читаются.
test('расшифровка работает с ротацией ключей', async () => {
const keys = ['new-key', 'old-key'];
const sealedWithOldKey = await Iron.seal(
{ role: 'admin' },
'old-key',
Iron.defaults
);
const unsealed = await Iron.unseal(
sealedWithOldKey,
{
...Iron.defaults,
keys
}
);
expect(unsealed.role).toBe('admin');
});
Здесь проверяется критический сценарий:
Важно убедиться, что порядок ключей влияет только на шифрование, но не ломает расшифровку.
test('приоритет ключа влияет на шифрование', async () => {
const keys = ['key-1', 'key-2', 'key-3'];
const sealed = await Iron.seal(
{ scope: 'user' },
keys[0],
Iron.defaults
);
const unsealed = await Iron.unseal(
sealed,
{
...Iron.defaults,
keys
}
);
expect(unsealed.scope).toBe('user');
});
Если изменить порядок ключей и оставить только старые в массиве, расшифровка должна продолжать работать до удаления ключа из rotation set.
Реальные системы редко переключаются мгновенно. Обычно применяется поэтапная схема:
Тестирование должно моделировать эти этапы.
function rotateKeys(oldKeys, newKey) {
return [newKey, ...oldKeys];
}
Тест:
test('эмуляция ротации ключей', async () => {
const oldKeys = ['key-v1', 'key-v2'];
const initial = await Iron.seal(
{ data: 123 },
oldKeys[0],
Iron.defaults
);
const rotated = rotateKeys(oldKeys, 'key-v3');
const result = await Iron.unseal(
initial,
{
...Iron.defaults,
keys: rotated
}
);
expect(result.data).toBe(123);
});
Один из наиболее критичных тестов — поведение при удалении ключа.
test('ошибка при удалении всех подходящих ключей', async () => {
const keys = ['new-key'];
const sealed = await Iron.seal(
{ id: 1 },
'old-key',
Iron.defaults
);
await expect(
Iron.unseal(sealed, {
...Iron.defaults,
keys
})
).rejects.toThrow();
});
Это гарантирует:
При миграции систем часто требуется проверить большое количество токенов, зашифрованных разными ключами.
test('массовая расшифровка при ротации', async () => {
const keys = ['k3', 'k2', 'k1'];
const tokens = await Promise.all([
Iron.seal({ id: 1 }, 'k1', Iron.defaults),
Iron.seal({ id: 2 }, 'k2', Iron.defaults),
Iron.seal({ id: 3 }, 'k3', Iron.defaults)
]);
const results = await Promise.all(
tokens.map(t =>
Iron.unseal(t, { ...Iron.defaults, keys })
)
);
expect(results.map(r => r.id)).toEqual([1, 2, 3]);
});
В распределённых системах ключи могут быть неконсистентны между инстансами. Это создаёт риск частичной расшифровки.
test('игнорирование неподходящих ключей в цепочке', async () => {
const keys = ['valid-key', 'invalid-key'];
const sealed = await Iron.seal(
{ flag: true },
'valid-key',
Iron.defaults
);
const result = await Iron.unseal(sealed, {
...Iron.defaults,
keys
});
expect(result.flag).toBe(true);
});
При автоматизированной проверке важно учитывать:
Пример матрицы:
| ключ шифрования | набор ключей при расшифровке | результат |
|---|---|---|
| k1 | [k1, k2, k3] | успех |
| k2 | [k1, k2, k3] | успех |
| k3 | [k1, k2, k3] | успех |
| k1 | [k2, k3] | ошибка |
При смене конфигурации Iron (например, обновление
defaults) важно убедиться, что ротация ключей не ломает
формат сериализации.
test('совместимость после изменения defaults', async () => {
const key = 'stable-key';
const sealed = await Iron.seal(
{ version: 1 },
key,
Iron.defaults
);
const unsealed = await Iron.unseal(
sealed,
{
...Iron.defaults,
keys: [key]
}
);
expect(unsealed.version).toBe(1);
});
В практических реализациях часто встречаются ошибки:
Каждая из этих ошибок приводит к деградации безопасности при реальной ротации.