Конвертация ключей Ed25519 в Curve25519 для шифрования

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

  • Ed25519 применяется для цифровых подписей (nacl.sign)
  • Curve25519 используется для обмена ключами и симметричного шифрования (nacl.box)

В TweetNaCl.js эти алгоритмы реализованы раздельно, и ключи между ними напрямую не совместимы без преобразования.


Почему требуется конвертация ключей

В реальных приложениях часто возникает сценарий:

  • пользователь хранит Ed25519 ключи для подписи
  • но также необходимо выполнять шифрование сообщений (nacl.box)

Проблема в том, что:

  • Ed25519 использует 64-байтовый секретный ключ
  • Curve25519 (X25519) использует 32-байтовый ключ

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


Структура ключей в TweetNaCl.js

Ed25519 (nacl.sign)

const keyPair = nacl.sign.keyPair();

Результат:

  • publicKey — 32 байта
  • secretKey — 64 байта (seed + public key)

Curve25519 (nacl.box)

const boxKeyPair = nacl.box.keyPair();

Результат:

  • publicKey — 32 байта
  • secretKey — 32 байта

Базовая идея конвертации

Конвертация строится на извлечении seed (первых 32 байт) из Ed25519 ключа.

Далее seed используется для построения Curve25519 ключа.


Получение Ed25519 ключа из seed

Самый корректный способ работы с конвертацией — начинать с seed:

const seed = nacl.randomBytes(32);

const edKeyPair = nacl.sign.keyPair.fromSeed(seed);

Здесь:

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

Конвертация Ed25519 → Curve25519

TweetNaCl.js не предоставляет официальную полноценную высокоуровневую функцию конвертации как libsodium, но на практике используется следующий подход.

Извлечение секретного ключа

const edKeyPair = nacl.sign.keyPair.fromSeed(seed);

const edSecretKey = edKeyPair.secretKey;

Получение Curve25519 ключа

const curveKeyPair = nacl.box.keyPair.fromSecretKey(edSecretKey);

Важное замечание о совместимости

В строгом криптографическом смысле:

  • Ed25519 secretKey ≠ Curve25519 secretKey

Однако в TweetNaCl.js используется упрощённая модель, где:

  • первые 32 байта seed могут быть использованы для генерации Curve25519 ключа
  • некоторые реализации игнорируют вторую половину ключа

Поэтому более безопасная практика:

const seed = edSecretKey.slice(0, 32);
const curveKeyPair = nacl.box.keyPair.fromSecretKey(seed);

Полный пример конвертации

// генерация Ed25519 ключей
const edKeyPair = nacl.sign.keyPair();

// извлечение seed
const seed = edKeyPair.secretKey.slice(0, 32);

// генерация Curve25519 ключей
const curveKeyPair = nacl.box.keyPair.fromSecretKey(seed);

// результат
console.log(curveKeyPair.publicKey);
console.log(curveKeyPair.secretKey);

Использование в шифровании nacl.box

После конвертации можно выполнять обмен сообщениями:

const message = new TextEncoder().encode("secret data");

const nonce = nacl.randomBytes(24);

const encrypted = nacl.box(
  message,
  nonce,
  curveKeyPair.publicKey,
  recipientCurvePublicKey
);

Обратная совместимость: Curve25519 → Ed25519

Обратное преобразование не является прямым.

Curve25519 ключ:

  • не содержит информации, необходимой для восстановления Ed25519 подписи

Поэтому:

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

Практическая архитектура ключей

Часто применяют следующую модель:

  • seed (32 байта) — корневой секрет

  • из него:

    • Ed25519 → подписи
    • Curve25519 → шифрование
const seed = nacl.randomBytes(32);

const signKeyPair = nacl.sign.keyPair.fromSeed(seed);

const boxKeyPair = nacl.box.keyPair.fromSecretKey(seed);

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

Использование полного 64-байтового ключа

nacl.box.keyPair.fromSecretKey(edSecretKey) // может работать некорректно

Проблема:

  • Curve25519 ожидает 32 байта
  • лишние данные могут приводить к некорректному ключу

Использование публичного ключа вместо secretKey

nacl.box.keyPair.fromSecretKey(edPublicKey) // ошибка

Безопасностные нюансы

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

Совместимость с libsodium

В libsodium существует явная функция:

  • crypto_sign_ed25519_sk_to_curve25519

TweetNaCl.js не предоставляет прямого аналога, поэтому:

  • используется ручное извлечение seed
  • либо сторонние адаптеры

Типовая структура модуля конвертации

export function ed25519ToCurve25519(edSecretKey) {
  const seed = edSecretKey.slice(0, 32);
  return nacl.box.keyPair.fromSecretKey(seed);
}

Применение в реальных системах

Конвертация используется в:

  • мессенджерах с единым идентификатором пользователя
  • P2P системах
  • децентрализованных протоколах
  • криптографических API поверх WebCrypto + NaCl

Взаимодействие с публичными ключами

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

  • Ed25519 publicKey ≠ Curve25519 publicKey
  • для шифрования всегда используется пара Curve25519

Модель универсального ключа

Некоторые системы используют подход:

  • один seed

  • два производных ключа:

    • signKeyPair
    • boxKeyPair

Это позволяет:

  • минимизировать хранение секретов
  • упростить архитектуру идентификации

Итоговая криптографическая модель

  • Ed25519 отвечает за целостность и подписи
  • Curve25519 отвечает за конфиденциальность
  • seed объединяет обе роли в единую точку генерации ключей