Шифрование сообщений
Шифрование сообщений

Привет, Хабр!

Я написал статью и преждевременно её публикую. Изначально планировал писать после завершения проекта, но до окончания осталось ещё пару месяцев, поэтому решил не терять время и написать статью пока информация свежа в голове. К тому же, в большей степени пишу для себя. :) В одном из моих последних проектов, который я разрабатываю как open source, я реализовал сквозное (end-to-end) шифрование — аналогично тому, как это делают, например, WhatsApp или Telegram.

В этой статье мы углубимся в реализацию шифрования сообщений на стороне клиента с использованием JavaScript и Web Crypto API, разобрав практический пример, который будет в самом конце статьи.

Начнём с того, что если вы полный ноль в криптографии, то понять написанное здесь может быть непросто. Мне самому, несмотря на 10 лет опыта в разработке, пришлось поломать голову — всё, что здесь происходит, это чистая математика, о которой мы в этой статье говорить не будем :) Особо впечатлительные могут подумать, что это магия :)

Если вкратце, без сложных слов и терминов, попробую объяснить суть сквозного (end-to-end) шифрования

Магия шифрования в трёх ключах

Три ключа — это основа, на которой строится шифрование end-to-end. Обратите внимание на ключ по центру — это важно.
Три ключа — это основа, на которой строится шифрование end-to-end. Обратите внимание на ключ по центру — это важно.

Фундамент, на котором всё держится, — это три ключа. Возвращайтесь к этому месту, если что-то окажется непонятным.

  1. Приватный ключ (Private Key): Хранится (в зашифрованном виде).

  2. Публичный ключ (Public Key): Доступен абсолютно всем.

  3. Общий секретный ключ (Shared Secret / Symmetric Key): Генерируется на основе вашего приватного ключа + публичного ключа вашего собеседника. Именно этот ключ используется для непосредственного шифрования и дешифрования сообщений.

Комбинация вашего приватного ключа + публичного ключа вашего собеседника позволяет получить общий секретный ключ (в нашем примере ниже это будет ключ AES). Благодаря этому общему секретному ключу вы можете шифровать и расшифровывать сообщения.

Приватный и публичный ключи можно хранить в базе данных, но есть нюанс с приватным ключом. Сам приватный ключ не рекомендуется хранить в открытом виде; его нужно дополнительно зашифровать паролем пользователя или любым другим ключевым словом (в рамках мессенджера это, как правило, пароль пользователя). Публичный ключ мы храним в открытом виде.

Общий секретный ключ (тот, что в коде нижеthis.aesKey) мы не храним в базе данных. Он генерируется (вычисляется) каждый раз при инициализации чата с конкретным контактом. Здесь может возникнуть недоумение: как же мы будем расшифровывать сообщения, если этот ключ не хранится, а генерируется заново? В этом и заключается "магия" асимметричного шифрования и протокола обмена ключами.

Общий ключ это "Ваш приватный ключ" + "Публичный ключ контакта"
Общий ключ это "Ваш приватный ключ" + "Публичный ключ контакта"

Когда вы открываете чат с контактом, ваш клиент заново вычисляет этот общий секретный ключ, как вы уже знаете вот таким образом (Ваш приватный ключ + Публичный ключ вашего контакта = Общий секретный ключ). С помощью этого ключа вы шифруете новые сообщения и расшифровываете все предыдущие сообщения в этом чате, так как они были зашифрованы тем же самым общим секретным ключом. Голову можно ломать долго и безрезультатно, пока не поймем, что такое асимметричное шифрование и протоколы обмена ключами.

Асимметричное шифрование и ECDH

Асимметричное шифрование использует пару ключей: публичный (public) и приватный (private). Публичный ключ можно свободно распространять, в то время как приватный ключ должен храниться его владельцем в секрете то есть в зашифрованном виде.

ECDH (Elliptic Curve Diffie-Hellman) – это протокол обмена ключами, основанный на математике эллиптических кривых. Он позволяет двум сторонам, каждая из которых имеет свою пару ECDH-ключей (приватный и публичный), установить общий секретный ключ через незащищенный канал. Важно, что третья сторона, даже перехватив их публичные ключи, не сможет вычислить этот общий секрет. В нашем примере используется кривая P-256 – популярный и надежный стандарт.

Думаю, немногие поняли то, что только что прочитали. Всё, что вам нужно понять на данном этапе, – это то, что технология работает :) Позже пазл сложится, возможно, после повторного прочтения. А теперь немного о встроенных технологиях браузера.

Web Crypto API

Web Crypto API – это встроенный в браузеры интерфейс JavaScript, предоставляющий доступ к низкоуровневым криптографическим примитивам. Он позволяет выполнять такие операции, как хеширование, генерация подписей, шифрование и дешифрование. Использование Web Crypto API предпочтительнее сторонних библиотек для основных криптографических операций, так как оно часто аппаратно ускорено и тщательно проверено на безопасность. Все операции Web Crypto API асинхронны и возвращают Promise.

А теперь перейдем к практическому анализу. Я создал класс ChatCrypto, который мы рассмотрим подробнее:

class ChatCrypto {
  
  constructor(myPrivateKeyBase64, theirPublicKeyBase64) {
    this.myPrivateKeyBase64 = myPrivateKeyBase64;
    this.theirPublicKeyBase64 = theirPublicKeyBase64;
    this.aesKey = null; // Здесь будет храниться общий симметричный ключ AES
  }

  static base64ToArrayBuffer(base64) {
    const binary = atob(base64);
    const bytes = new Uint8Array(binary.length);
    for (let i = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer;
  }

  static arrayBufferToBase64(buffer) {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let b of bytes) {
      binary += String.fromCharCode(b);
    }
    return btoa(binary);
  }

  init() {
    
    // Преобразуем ключи из Base64 в ArrayBuffer
    const privateRaw = ChatCrypto.base64ToArrayBuffer(this.myPrivateKeyBase64);
    const publicRaw = ChatCrypto.base64ToArrayBuffer(this.theirPublicKeyBase64);

    // Замечание: Следующие строки для разбора publicRaw на x и y координаты,
    // и сборка uncompressedPoint могут быть специфичны для определенного формата
    // представления "сырого" открытого ключа. Если publicRaw уже в формате SPKI,
    // они могут не понадобиться, так как crypto.subtle.importKey("spki", ...)
    // ожидает стандартную структуру.
    // const x = publicRaw.slice(0, publicRaw.byteLength / 2);
    // const y = publicRaw.slice(publicRaw.byteLength / 2);
    // const uncompressedPoint = new Uint8Array([0x04, ...new Uint8Array(x), ...new Uint8Array(y)]);

    // Импортируем наш приватный ключ
    return crypto.subtle.importKey(
      "pkcs8", // Формат приватного ключа (стандартный)
      privateRaw,
      { name: "ECDH", namedCurve: "P-256" }, // Алгоритм и параметры
      false, // Неэкспортируемый
      ["deriveBits"] // Разрешенное использование: для вывода бит (общего секрета)
    ).then(privateKey => {
      // Импортируем публичный ключ собеседника
      return crypto.subtle.importKey(
        "spki", // Формат публичного ключа (стандартный)
        publicRaw,
        { name: "ECDH", namedCurve: "P-256" },
        false, // Неэкспортируемый
        [] // Для публичного ключа в ECDH здесь специфические использования не нужны
      ).then(publicKey => {
        // 4. Вычисляем общий секрет (deriveBits)
        return crypto.subtle.deriveBits(
          { name: "ECDH", public: publicKey }, // Указываем публичный ключ собеседника
          privateKey, // Наш приватный ключ
          256 // Длина выводимого секрета в битах
        );
      });
    }).then(sharedBits => {
      // Хешируем общий секрет для получения ключа AES (используем SHA-256 как KDF)
      return crypto.subtle.digest("SHA-256", sharedBits);
    }).then(hashed => {
      // Импортируем хешированный секрет как ключ AES-GCM
      return crypto.subtle.importKey(
        "raw", // Формат "сырых" байт
        hashed, // Хешированный секрет
        { name: "AES-GCM" }, // Алгоритм симметричного шифрования
        false, // Неэкспортируемый
        ["encrypt", "decrypt"] // Разрешенные использования: шифрование и дешифрование
      );
    }).then(aesKey => {
      this.aesKey = aesKey; // ✅ ВАЖНО! Сохраняем полученный ключ AES
      return true;         // Сигнализируем об успешной инициализации
    });
  }

  encrypt(plaintext) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Генерируем уникальный вектор инициализации (IV)
    const iv = crypto.getRandomValues(new Uint8Array(12)); // 12 байт (96 бит) рекомендуется для AES-GCM
    
    // Преобразуем текстовое сообщение в байты (UTF-8)
    const encoded = new TextEncoder().encode(plaintext);

    // Шифруем данные
    return crypto.subtle.encrypt(
      { name: "AES-GCM", iv: iv }, // Алгоритм и IV
      this.aesKey, // Наш общий ключ AES
      encoded // Данные для шифрования
    ).then(encrypted => {
      // 4. Возвращаем IV и зашифрованные данные (в Base64 для удобства передачи)
      return {
        iv: ChatCrypto.arrayBufferToBase64(iv),
        data: ChatCrypto.arrayBufferToBase64(encrypted)
      };
    });
  }

  decrypt(cipherBase64, ivBase64) {
    if (!this.aesKey) return Promise.reject("ChatCrypto not initialized");

    // Преобразуем шифротекст и IV из Base64 в ArrayBuffer
    const encrypted = ChatCrypto.base64ToArrayBuffer(cipherBase64);
    const ivBuffer = ChatCrypto.base64ToArrayBuffer(ivBase64); 

    // Дешифруем данные
    return crypto.subtle.decrypt(
      { name: "AES-GCM", iv: new Uint8Array(ivBuffer) }, // Алгоритм и IV (должен быть TypedArray)
      this.aesKey, // Тот же общий ключ AES
      encrypted // Зашифрованные данные
    ).then(decrypted => {
      // Преобразуем расшифрованные байты обратно в строку
      return new TextDecoder().decode(decrypted);
    });
  }
}

Конструктор constructor() и метод base64ToArrayBuffer():

  • Конструктор принимает ваш приватный ключ и публичный ключ собеседника в формате Base64. Base64 – это способ кодирования бинарных данных в текстовую строку, удобный для передачи или хранения.

  • this.aesKey инициализируется как null и будет заполнен после успешного выполнения метода init().

  • Статические методы base64ToArrayBuffer и arrayBufferToBase64 служат для преобразования данных между строками Base64 и ArrayBuffer (формат, с которым работает Web Crypto API).

Метод init(): Установление общего ключа AES

Это сердце нашего класса, где происходит "магия" ECDH и создается общий ключ для симметричного шифрования.

Разбор шагов в init():

  1. Преобразование ключей: Ключи из Base64 переводятся в ArrayBuffer.

  2. Импорт приватного ключа: Ваш приватный ключ импортируется в формате pkcs8. Указывается, что это ключ ECDH на кривой P-256 и он будет использоваться для deriveBits (вычисления общего секрета).

  3. Импорт публичного ключа собеседника: Публичный ключ собеседника импортируется в формате spki.

  4. Вычисление общего секрета (deriveBits): Это ключевой шаг ECDH. Используя ваш приватный ключ и публичный ключ собеседника, deriveBits вычисляет общий секретный набор бит (sharedBits). Этот секрет будет одинаковым у вас и вашего собеседника, если они используют свои соответствующие приватные ключи и публичные ключи друг друга.

  5. Хеширование общего секрета (digest): sharedBits хешируются с помощью SHA-256. Это распространенная практика для преобразования вывода deriveBits в криптографически стойкий ключ нужной длины для симметричного шифра (в данном случае AES). Этот шаг также служит как KDF (Key Derivation Function).

  6. Импорт ключа AES (importKey): Полученный хеш (hashed) импортируется как "сырой" (raw) ключ для алгоритма AES-GCM. Этот ключ (this.aesKey) теперь готов к использованию для шифрования и дешифрования сообщений.

После успешного выполнения this.aesKey будет содержать объект CryptoKey, готовый к работе.

Метод encrypt(plaintext): Шифрование сообщения

Разбор шагов в encrypt():

  1. Генерация IV (Initialization Vector): Создается случайный 12-байтный IV. Напоминаем, он должен быть уникальным для каждого шифрования этим же ключом.

  2. Кодирование текста: Сообщение преобразуется из строки JavaScript в Uint8Array (последовательность байт в кодировке UTF-8) с помощью TextEncoder.

  3. Шифрование: crypto.subtle.encrypt выполняет шифрование данных с использованием AES-GCM, нашего this.aesKey и сгенерированного iv.

  4. Возврат результата: Зашифрованные данные и IV (оба в Base64) возвращаются как объект. IV необходимо передать получателю вместе с шифротекстом, так как он потребуется для дешифрования.

Метод decrypt(cipherBase64, ivBase64): Дешифрование сообщения

Разбор шагов в decrypt():

  1. Преобразование данных: Полученные шифротекст и IV (в Base64) преобразуются обратно в ArrayBuffer. Обратите внимание, что для crypto.subtle.decrypt параметр iv должен быть TypedArray (например, Uint8Array), поэтому мы передаем new Uint8Array(ivBuffer).

  2. Дешифрование: crypto.subtle.decrypt выполняет дешифрование. Важно, что AES-GCM не только расшифрует данные, но и проверит их целостность и аутентичность, используя тот же aesKey и iv, которые использовались при шифровании. Если данные были подделаны, ключ не тот или IV не тот, метод decrypt вернет ошибку (отклонит Promise).

  3. Декодирование текста: Успешно расшифрованные байты преобразуются обратно в читаемую строку с помощью TextDecoder.

4. Как это работает вместе: Концептуальный поток

  1. Генерация ключевых пар:

    • Пользователь А генерирует свою пару ECDH-ключей (публичный PkA и приватный SkA).

    • Пользователь Б делает то же самое (публичный PkB и приватный SkB).

    • Этот шаг в приведенном коде ChatCrypto не показан, но показан ниже в секции примеры (он выполняется один раз, например, при регистрации пользователя), он предшествует использованию класса. Web Crypto API имеет метод crypto.subtle.generateKey для этого. Приватный ключ Sk должен храниться надежно и быть зашифрован паролем пользователя.

  2. Обмен публичными ключами:

    • Пользователь А передает свой публичный ключ PkA пользователю Б.

    • Пользователь Б передает свой публичный ключ PkB пользователю А.

    • Этот обмен должен быть надежным, чтобы избежать атаки "человек посередине" (Man-in-the-Middle, MitM). Например, через защищенный сервер или путем верификации отпечатков ключей.

  3. Инициализация ChatCrypto и вычисление общего секретного ключа:

    • У пользователя А: const cryptoA = new ChatCrypto(SkA_base64, PkB_base64); await cryptoA.init();

    • У пользователя Б: const cryptoB = new ChatCrypto(SkB_base64, PkA_base64); await cryptoB.init();

    • В результате у обоих (cryptoA.aesKey и cryptoB.aesKey) будет вычислен одинаковый симметричный ключ AES.

  4. Обмен сообщениями:

    • Пользователь А шифрует сообщение для Б: const { iv, data } = await chatCryptoA.encrypt("Привет, Б!"); Затем А отправляет объект { iv, data } пользователю Б.

    • Пользователь Б получает { iv, data } и дешифрует: const message = await chatCryptoB.decrypt(data, iv); // message будет "Привет, Б!"

Пример: Шифрование текста encrypt()

// Иницилизация класса
let chatCrypto = new ChatCrypto( "Мой_приватный_ключ" , "Публичный_ключ_контакта" );

// Запускаем
chatCrypto.init().then(() => {
   chatCrypto.encrypt("Текст").then(result => {

     // Выведется зашифрованный текст
      console.log(result);
    
  });
});

Пример: Расшифровка текста decrypt()

// Иницилизация класса
let chatCrypto = new ChatCrypto( "Мой_приватный_ключ" , "Публичный_ключ_контакта" );

// Запускаем
chatCrypto.init()
  .then( () => chatCrypto.decrypt("Зашифрованный_текст", "Векторный_ключ") )
  .then(result => {
  
    // Выведется расшифрованный текст
    console.log(result);
  
  })

Заключение

Мы рассмотрели, как можно реализовать надежное сквозное шифрование сообщений в JavaScript с использованием Web Crypto API. Комбинация ECDH для безопасного обмена ключами и AES-GCM для эффективного и аутентифицированного шифрования данных является мощным и современным подходом. Класс ChatCrypto служит хорошим отправным примером такой реализации. Помните о важности безопасной генерации, хранения приватных ключей и надежного обмена публичными ключами для построения безопасной системы.

Комментарии (50)


  1. supercat1337
    29.05.2025 21:38

    Спасибо, хорошая статья! Можно еще добавить, что web crypto доступен и в принципе рекомендован к использованию в воркерах, чтобы не замедлять ui во время работы с криптографией. На маленьких сообщениях это незаметно, а вот при работе с большими файлами там другая история.


    1. nin-jin
      29.05.2025 21:38

      Он и так в отдельном потоке исполняется, потому функции и асинхронные.


      1. Krokochik
        29.05.2025 21:38

        Пул потоков-то ограничен. В реальности у вас ui будет из-за subtle стоять в очереди, если не вынести в воркер


        1. nin-jin
          29.05.2025 21:38

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


          1. Krokochik
            29.05.2025 21:38

            У меня в приложении ui по 3 секунды висел из-за сложного pbkdf. Я научен)


            1. nin-jin
              29.05.2025 21:38

              Видимо вы в рендерере влепили await, из-за чего он остановился в ожидании ответа от крипто-треда. И не разобравшись подумали, что криптография исполняется в ui-треде.


              1. Krokochik
                29.05.2025 21:38

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


                1. nin-jin
                  29.05.2025 21:38

                  Давайте уже свои ссылки по теме, не томите общественность.


                1. Ekhidirov Автор
                  29.05.2025 21:38

                  Тут я с вами соглашусь пул потоков действительно ограничен. В идеале всю тяжёлую логику стоит перенести в веб воркеры, чтобы основной поток был занят только обработкой пользовательского ввода, включая клики. Это особенно заметно на бюджетных мобильных устройствах за $100, где часто наблюдаются микролаги и прочие задержки. Воркеры могут помочь с этой проблемой.

                  Что касается WebCrypto, он выполняется вне основного потока и никак на него не влияет. Таким образом, если вернуться к теме статьи, шифрование и дешифрование именно сообщений обходится недорого даже для слабых устройств.


                  1. Krokochik
                    29.05.2025 21:38

                    Я писал больше про тяжелые и долгие операции, которые специально замедленны, вроде pbkdf. Расшифровка/шифрование, когда необходимости в усложнении нет, т.к. токены итак затруднительно подобрать, даже при большом объеме данных не должны вызывать проблем. Максимум подготовка данных, но это на совсем слабых устройствах


  1. nin-jin
    29.05.2025 21:38

    Привет HabrGPT, перепиши код с промисов на асинхронные функции, вынеси функции работы с base64 в отдельный неймспейс, используй base64url кодирование, для генерации общего ключа используй функцию sharedKey, в качестве вектора инициализации используй хеш от сообщения и публичного ключа, для импорта/экспорта используй формат JWK.


    1. valyasha19
      29.05.2025 21:38

      Что за HabrGPT?


      1. nin-jin
        29.05.2025 21:38

        Автор большинства статей на Хабре в последнее вюемя.


        1. valyasha19
          29.05.2025 21:38

          Ну вы хотябы на вопрос можете ответить что такое HabrGPT? Нейросеть хабра или что? В интернете не нашел инфу.


          1. supercat1337
            29.05.2025 21:38

            Шутливое обращение к абстрактному автору


    1. nin-jin
      29.05.2025 21:38

      Не sharedKey, а deriveKey, конечно же.


      1. nazar_nazar
        29.05.2025 21:38

        Не deriveKey а deriveBits в рамках класса который в статье


        1. nin-jin
          29.05.2025 21:38

          Вот в рамках конкретно этого класса должен быть именно deriveKey. В отличие от $mol_crypto_sacred, который является обёрткой над буфером, а не нативным ключом.


  1. Vic1111
    29.05.2025 21:38

    скажите, возможно ли шифрование голоса во время звонка на кнопрочных телефонах J2ME причем не через сервер а точка - точка ?


    1. eimrine
      29.05.2025 21:38

      Вы от него ещё и реал-тайм хотите? Скорее можно сказать фразу и долго-долго ждать, после того как притащите туда хоть какой-то современный кодек который по крайней мере уменьшит количество байт на зашифровку. Во время звонка я не уверен что можно с ж2ме пользоваться звуковым потоком.


      1. Vic1111
        29.05.2025 21:38

        ответ Алисы:

        Нет, шифрование голоса на Java не сопровождается сильной задержкойstackoverflow.comvk.com

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

        Cipher

         с алгоритмом AES. Задержка в процессе шифрования не превышает нескольких миллисекунд. sky.provk.comstackoverflow.comvk.com

        Это достигается за счёт оптимизации алгоритмов и использования современных методов, таких как режим CBC (Cipher Block Chaining), который позволяет обрабатывать данные частями. 


  1. eungenue
    29.05.2025 21:38

    Главные шпионы в наше время - это устройства, на которых всё это замечательное «шифрование» происходит. Там и ОС закрытая, и постоянная неотключаемая связь с настоящим хозяином, и пароли все эти ваши. А так - ну, можно и пошифровать, батарейку и проц разогреть.


    1. Vic1111
      29.05.2025 21:38

      именно так, после дистанционного заражения смартфона не важно какое у вас крутое шифрование или какой меcседжер. Поэтому если смартфон то шифрование например голоса должно быть вынесено за пределы телефона, это блютузная например приставка.


  1. Chupaka
    29.05.2025 21:38

    А как пользователи-то ключами управляют? Например, передать публичный через сервер — ещё куда ни шло, а вот приватный после логина в приложение откуда берётся на устройстве, чтобы расшифровать все сообщения, отправленные, пока ты офлайн был?


    1. Nexoic
      29.05.2025 21:38

      Его сохранять надо в памяти после логина на клиенте и не сохранять в бд или где то если по честному делать


      1. Chupaka
        29.05.2025 21:38

        Я же о том и спрашиваю: если я залогинюсь на чистом устройстве, то все сообщения, которые мне отправляли до этого момента, я не получу, поскольку сгенерируется новый ключ, а они были отправлены старому?


        1. supercat1337
          29.05.2025 21:38

          Зависит от реализации. Может приватный ключ кто-то создаёт из пароля и соли.


    1. Ekhidirov Автор
      29.05.2025 21:38

      Обратите внимание, что приватный ключ дополнительно шифруется. В качестве примера его можно зашифровать паролем пользователя при регистрации аккаунта или любым другим ключевым словом, известным только вам. После шифрования приватный ключ сохраняется в базе данных. При авторизации клиенту передаётся только зашифрованный приватный ключ, который он расшифровывает с помощью пароля, введённого при авторизации. Затем ключ сохраняется на устройстве пользователя до окончания сессии авторизации. Но если пользователь забудет свой пароль от своей учетной записи, возникает проблема он больше не сможет расшифровать свои старые сообщения.


      1. antirek
        29.05.2025 21:38

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


        1. Ekhidirov Автор
          29.05.2025 21:38

          Да, всё верно если пароль утерян, восстановить сообщения уже не получится. Но это лишь один из возможных сценариев. Приватный ключ можно шифровать не только паролем пользователя, но и другими ключевыми словами. Например, это могут быть recovery codes, которые система случайным образом генерирует и затем использует для шифрования приватного ключа.

          Допустим, сгенерировано 6 recovery codes тогда система создаёт 6 зашифрованных копий приватного ключа, каждая из которых связана с одним из этих кодов. Эти копии необходимо дополнительно хранить, например, в базе данных. При смене пароля пользователю стоит предлагать восстановить сообщения с помощью recovery codes и заранее обратить внимание на важность их сохранения ...


          1. nin-jin
            29.05.2025 21:38

            С тем же успехом можно и приватный ключ в открытом виде где-то "важно сохранять".


            1. xidisidi
              29.05.2025 21:38

              Смысл тогда заниматься шифрованием сообщений если приватные ключи где то будут храниться в открытом виде? Это будет не справедливо по отношению к пользователям которые будут думать что их сообщения шифруются енд ту енд. Если приватные ключи будут храниться в открытом виде значит кто то будет иметь возможность расшифровать любые сообщения вопрос возникает тогда кто? Админ, владелец хостинга, хакер, сотрудник который по ошибке слил базу и т д?


              1. nin-jin
                29.05.2025 21:38

                Вы не поняли, тут речь не про хранение в базе, а про хранение в надёжном месте у пользователя. Ну там распечатать и в сейф или типа того.


                1. defin85
                  29.05.2025 21:38

                  А пока выводишь на печать, потихонечку себе сохраняешь


                  1. nin-jin
                    29.05.2025 21:38

                    С тем же успехом "себе сохранять" можно и без вывода на печать.


  1. ckpunT
    29.05.2025 21:38

    После прочтения Асимметричное шифрование и ECDH магия осталась магией из-за пропущенного процесса вычисления общего секретного ключа. Потому что

    Голову можно ломать долго и безрезультатно, пока не поймем, что такое асимметричное шифрование и протоколы обмена ключами.

    Если не погружаться в самые дебри, то:

    У Алисы есть приватный ключ (a) и публичный ключа (A). Публичный ключ высчитывается из приватного ключа A = a x G

    У Боба так же приватный (b) и публичный (B = b x G)

    G – это базовая точка на эллиптической кривой, общая для всех. Алиса и Боб заранее договорились какую кривую будут использовать. За нас уже всё посчитано, точка описана в стандарте, просто берем её оттуда.

    Алиса и Боб обменялись публичными ключами и каждый из них умножает свой приватный ключ на публичный ключ собеседника:

    Алиса: a x B => a x (b x G) = ab x G

    Боб: b x A => b x (a x G) = ba x G => ab x G

    Вот и вся "магия" получения общего секретного ключа


    1. cordova07
      29.05.2025 21:38

      Это можно как то протестировать? Если честно ничего не понял)


      1. ckpunT
        29.05.2025 21:38

        более простого объяснения не знаю

        from math import gcd
        from sympy import primefactors
        
        
        def is_primitive_root(g, p):
            if gcd(g, p) != 1:
                return False
            order = p - 1
            for q in primefactors(order):
                if pow(g, order // q, p) == 1:
                    return False
            return True
        
        
        def min_primitive_root(p):
            for g in range(2, p):
                if is_primitive_root(g, p):
                    return g
        
        
        if __name__ == '__main__':
            sess_const = 23  # Большое простое число (в реальности 2048+ бит)
            pr = min_primitive_root(sess_const)  # Первообразный корень по модулю sess_const
        
            a = 6  # Секретный ключ Алисы
            A = pow(pr, a, sess_const)  # Публичный ключ Алисы (отправляется Бобу).
        
            b = 15  # Секретный ключ Боба
            B = pow(pr, b, sess_const)  # Публичный ключ Боба (отправляется Алисе)
        
            K_alice = pow(B, a, sess_const)
            K_bob = pow(A, b, sess_const)
        
            print(f"Публичные параметры: p = {sess_const}, g = {pr}")
            print(f"Алиса: приватный a = {a}, публичный A = {A}")
            print(f"Боб: приватный b = {b}, публичный B = {B}")
            print(f"Общий секретный ключ у Алисы: {K_alice}")
            print(f"Общий секретный ключ у Боба: {K_bob}")
        

        Здесь есть подробное объяснение как это работает, но без подготовки это скорее всего не осилить.
        Тут можно закончить словами автора: Всё, что вам нужно понять на данном этапе, – это то, что технология работает