Сериализация и десериализация ключей ECC

ECC-ключи в SJCL представляют собой объекты над эллиптическими кривыми, где основная криптографическая сущность разделяется на публичную и приватную части. Публичная часть описывает точку на кривой (x, y), приватная — скаляр (экспоненту), используемый для генерации этой точки. При переносе ключей между сессиями, хранилищами или сервисами возникает задача их сериализации и последующей десериализации.

В Stanford JavaScript Crypto Library ключи ECC реализованы через модуль sjcl.ecc. Чаще всего используется схема ElGamal:

  • sjcl.ecc.elGamal.publicKey — публичный ключ
  • sjcl.ecc.elGamal.secretKey — приватный ключ
  • sjcl.ecc.curves — набор параметров кривых (например, c256)

Публичный ключ содержит точку на кривой:

  • координата X (bn)
  • координата Y (bn)
  • ссылка на кривую

Приватный ключ хранит:

  • секретный экспонент (bn)
  • публичный ключ (как производное значение)

Причины сериализации

Сериализация необходима для:

  • хранения ключей в localStorage или IndexedDB
  • передачи ключей по сети (API, WebSocket)
  • резервного копирования
  • восстановления сессий шифрования

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

Базовые принципы сериализации

ECC-ключ нельзя сохранять напрямую как объект JavaScript. Он содержит сложные типы (sjcl.bn, точки кривой), которые не сериализуются через JSON.stringify.

Поэтому используется промежуточное представление:

  • числа → строка (hex / base64 / decimal)
  • точки → пара координат (x, y)
  • параметры кривой → строковый идентификатор

Сериализация публичного ключа

Публичный ключ состоит из координат точки:

function serializePublicKey(pub) {
  return {
    curve: pub._curve.name,
    x: pub._point.x.toString(),
    y: pub._point.y.toString()
  };
}

В некоторых реализациях SJCL доступ к точке осуществляется через методы get():

const point = pub.get();

Тогда сериализация выглядит так:

function serializePublicKey(pub) {
  const point = pub.get();
  return {
    curve: pub._curve.name,
    x: point.x.toString(16),
    y: point.y.toString(16)
  };
}

Использование hex-формата предпочтительно для компактности и совместимости.

Сериализация приватного ключа

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

function serializeSecretKey(sec) {
  return {
    curve: sec._curve.name,
    exponent: sec._exponent.toString(16),
    pub: serializePublicKey(sec._k.pub)
  };
}

Важно сохранять публичную часть внутри приватного ключа, так как она используется при восстановлении объекта.

Полная сериализация ключевой пары

Часто ключи сохраняются вместе:

function serializeKeyPair(pair) {
  return JSON.stringify({
    pub: serializePublicKey(pair.pub),
    sec: serializeSecretKey(pair.sec)
  });
}

На практике результат дополнительно кодируют в base64:

function encodeKeyPair(pair) {
  return btoa(serializeKeyPair(pair));
}

Десериализация публичного ключа

Для восстановления ключа необходимо заново создать точку на кривой:

function deserializePublicKey(data) {
  const curve = sjcl.ecc.curves[data.curve];
  const point = new sjcl.ecc.point(
    curve,
    sjcl.bn.fromBits(sjcl.codec.hex.toBits(data.x)),
    sjcl.bn.fromBits(sjcl.codec.hex.toBits(data.y))
  );

  return new sjcl.ecc.elGamal.publicKey(curve, point);
}

Если используется строковое представление без bitArray:

const x = new sjcl.bn(data.x, 16);
const y = new sjcl.bn(data.y, 16);

Десериализация приватного ключа

Приватный ключ требует восстановления скаляра и привязки к публичному ключу:

function deserializeSecretKey(data) {
  const curve = sjcl.ecc.curves[data.curve];

  const exponent = new sjcl.bn(data.exponent, 16);
  const pub = deserializePublicKey(data.pub);

  return new sjcl.ecc.elGamal.secretKey(curve, exponent, pub);
}

Работа с форматами кодирования

SJCL активно использует bitArray и codec:

  • sjcl.codec.hex
  • sjcl.codec.base64
  • sjcl.codec.bytes

Альтернативный подход — хранить значения через bitArray:

const xBits = sjcl.codec.hex.toBits(xHex);
const x = sjcl.bn.fromBits(xBits);

Base64 используется при передаче через сеть:

const encoded = sjcl.codec.base64.fromBits(bits);
const decoded = sjcl.codec.base64.toBits(encoded);

Важные особенности представления bn

Тип sjcl.bn не является примитивом. Его свойства:

  • поддерживает большие числа (Big Integer)
  • имеет методы .toString(radix)
  • может быть восстановлен из bitArray
  • не совместим с JSON напрямую

Ошибка, которая часто возникает:

JSON.stringify(bn) // => {}

Поэтому всегда требуется явное преобразование.

Привязка ключа к кривой

Кривая является критически важным параметром:

  • c192
  • c224
  • c256
  • c384
  • c521

Без точного совпадения кривой восстановление ключа невозможно:

const curve = sjcl.ecc.curves["c256"];

Любое несоответствие приводит к ошибкам при криптографических операциях или некорректной проверке подписи.

Безопасное хранение сериализованных ключей

При хранении сериализованных данных необходимо учитывать:

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

Типичный подход:

  1. сериализация ключа
  2. шифрование через sjcl.encrypt
  3. сохранение зашифрованного текста

Типовые ошибки при десериализации

На практике встречаются следующие проблемы:

Потеря точности при конвертации

Использование Number вместо bn приводит к разрушению ключа.

Несовпадение формата hex/base64

Попытка интерпретировать base64 как hex приводит к неверным координатам.

Игнорирование кривой

Создание точки без привязки к curve делает ключ некорректным.

Неправильная сборка point

ECC точка должна быть строго валидна для заданной кривой.

Оптимизированный формат хранения

Для продакшн-систем часто используют компактный формат:

{
  c: "c256",
  x: "a91f...",
  y: "02bc...",
  d: "ff01..." 
}

Где:

  • c — кривая
  • x, y — публичная точка
  • d — приватный скаляр

Такой формат легко сериализуется, минимален по размеру и удобен для передачи.

Восстановление ключей в прикладных системах

В системах с сессиями или токенами восстановление ключей происходит на этапе инициализации криптографического контекста:

  • загрузка JSON
  • декодирование значений
  • восстановление bn и point
  • создание объектов publicKey и secretKey

После этого ключи становятся пригодными для:

  • ECDH обмена
  • подписи сообщений
  • проверки подписи
  • генерации общего секрета