Установка и чтение Cookie вручную

Библиотека Iron в экосистеме JavaScript используется для шифрования и подписи данных, чаще всего — для безопасного хранения информации в cookie. В Node.js она распространяется через npm и устанавливается как отдельный модуль.

Установка выполняется стандартной командой:

npm install @hapi/iron

После установки модуль становится доступным для подключения через require или import, в зависимости от используемой системы модулей.

const Iron = require('@hapi/iron');

В ESM-окружении:

import Iron from '@hapi/iron';

Основная концепция работы Iron

Библиотека реализует механизм «запечатывания» (seal) и «распечатывания» (unseal) данных. В отличие от обычного шифрования, результатом является строка, которая одновременно:

  • защищена от чтения
  • защищена от подмены
  • может содержать сериализованные объекты

Чаще всего используется для безопасного хранения JSON-данных в cookie, когда необходимо исключить возможность их изменения на стороне клиента.

Установка секретного ключа

Любая операция с Iron требует криптографического ключа. Он должен быть достаточно сложным и храниться на сервере.

Пример ключа:

const key = 'a-very-long-and-secure-random-secret-key';

В реальных приложениях ключ обычно берётся из переменных окружения:

const key = process.env.IRON_SECRET;

Перед записью в cookie данные преобразуются в защищённую строку с помощью метода seal.

const Iron = require('@hapi/iron');

const sessionData = {
  userId: 42,
  role: 'admin'
};

const password = 'a-very-long-and-secure-random-secret-key';

async function createCookieValue() {
  const sealed = await Iron.seal(sessionData, password, Iron.defaults);
  return sealed;
}

Результат — строка, содержащая зашифрованные данные и метаданные целостности.

После получения зашифрованного значения его можно записать в HTTP-ответ:

const http = require('http');

http.createServer(async (req, res) => {
  const password = process.env.IRON_SECRET;

  const data = {
    userId: 42,
    role: 'admin'
  };

  const sealed = await Iron.seal(data, password, Iron.defaults);

  res.setHeader('Set-Cookie', `session=${sealed}; HttpOnly; Secure; Path=/`);
  res.end('Cookie установлен');
});

Флаги cookie:

  • HttpOnly — запрет доступа из JavaScript
  • Secure — передача только по HTTPS
  • Path=/ — доступность на всём сайте

Для извлечения данных необходимо:

  1. Получить cookie из заголовка запроса
  2. Извлечь значение
  3. Распаковать через Iron.unseal
function parseCookies(req) {
  const header = req.headers.cookie;
  const result = {};

  if (!header) {
    return result;
  }

  header.split(';').forEach(cookie => {
    const parts = cookie.split('=');
    const key = parts[0].trim();
    const value = parts[1];
    result[key] = value;
  });

  return result;
}

Расшифровка значения

const Iron = require('@hapi/iron');

async function readCookie(req) {
  const cookies = parseCookies(req);
  const sealed = cookies.session;

  if (!sealed) {
    return null;
  }

  const password = process.env.IRON_SECRET;

  const unsealed = await Iron.unseal(sealed, password, Iron.defaults);
  return unsealed;
}

После выполнения unseal возвращается исходный объект:

{
  userId: 42,
  role: 'admin'
}

Внутренний формат защищённой строки

Строка, создаваемая Iron, содержит несколько слоёв защиты:

  • сериализованные данные
  • HMAC-подпись
  • шифрование содержимого
  • служебные параметры алгоритма

Это делает невозможным:

  • изменение данных без ключа
  • подделку структуры
  • частичное восстановление информации

Использование кастомных параметров безопасности

Iron позволяет задавать параметры защиты через options.

const options = {
  ttl: 24 * 60 * 60 * 1000, // 1 день
  timestampSkewSec: 60,
  localtimeOffsetMsec: 0
};

Применение:

const sealed = await Iron.seal(data, password, options);

TTL ограничивает срок жизни данных, после чего unseal вызовет ошибку.

Обработка ошибок при распаковке

При работе с cookie важно учитывать возможные ошибки:

try {
  const data = await Iron.unseal(sealed, password, Iron.defaults);
} catch (err) {
  // данные повреждены или истёк срок действия
}

Типичные причины ошибок:

  • изменение cookie клиентом
  • неверный секретный ключ
  • истечение TTL
  • повреждение строки при передаче

Интеграция с HTTP-сервером без фреймворков

Пример полного цикла работы:

const http = require('http');
const Iron = require('@hapi/iron');

const secret = process.env.IRON_SECRET;

http.createServer(async (req, res) => {
  const cookies = parseCookies(req);

  let session = null;

  if (cookies.session) {
    try {
      session = await Iron.unseal(cookies.session, secret, Iron.defaults);
    } catch (e) {
      session = null;
    }
  }

  if (!session) {
    const newSession = {
      userId: 1,
      role: 'guest'
    };

    const sealed = await Iron.seal(newSession, secret, Iron.defaults);

    res.setHeader('Set-Cookie', `session=${sealed}; HttpOnly; Path=/`);
    res.end('Новая сессия создана');
    return;
  }

  res.end(`Пользователь ${session.userId}`);
}).listen(3000);

Особенности практического применения

Использование Iron в cookie-хранилище отличается от обычного шифрования тем, что:

  • данные не хранятся в открытом виде
  • сервер не требует отдельного хранилища сессий
  • состояние пользователя полностью находится на стороне клиента, но защищено криптографически

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