Реализация алгоритма RSA в архитектуре «клиент-сервер»

    Возникновение потребности




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

    В статье будут рассмотрены варианты реализации алгоритма RSA на клиент-серверной архитектуре, и пример такой реализации в реальном проекте.


    Концепция алгоритма RSA


    Я не буду описывать здесь особенности данного алгоритма, а расскажу именно о том, как его можно использовать в клиент-серверной архитектуре.
    Небольшое вступление… Собственно, RSA (буквенная аббревиатура от фамилий Rivest-Shamir-Adleman) – это криптографический алгоритм с открытым ключом. Это значит, что системой генерируется два разных ключа – открытый и секретный.
    • Открытый ключ передается по открытому (незащищенному) каналу, и используется для зашифровки данных.
    • Секретный же ключ хранится только у владельца, и используется для расшифровки любых данных, зашифрованных открытым ключом.
    Таким образом, мы можем передавать открытый ключ кому угодно, и получать зашифрованные этим ключом сообщения, расшифровать которые можем только мы (с использованием секретного ключа).

    Рисунок №1. Концепция открытого ключа

    Данная концепция представлена на рисунке №1, изображенном выше.

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

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

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

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


    Клиент — сервер



    Итак, для начала определимся с ключами. Как вы помните, для зашифровки сообщений необходим открытый ключ получателя. Соответственно, серверу необходим открытый ключ клиента, а клиенту – открытый ключ сервера. Следовательно, перед началом передачи данных, необходимо произвести обмен ключами. Как это происходит, рассмотрим на рисунке №2, в котором представлен процесс обмена ключами.



    1. Клиент открывает соединение с сервером и генерирует связку ключей (открытый-секретный); далее он отправляет пакет серверу, в котором передает ему свой открытый ключ;
    2. Сервер принимает пакет, считывает и сохраняет открытый ключ клиента, генерирует собственную связку ключей; после этого он отправляет пакет клиенту, в котором передает ему свой открытый ключ;
    3. Клиент принимает пакет, считывает и сохраняет открытый ключ сервера;


    Обмен завершен в три этапа. Теперь как у сервера, так и у клиента имеется открытый ключ собеседника «на том конце линии». Однако тут сразу необходимо выбрать одно из двух решений поводу того, как сервер будет генерировать ключи для своих клиентов:


    1. Сервер генерирует один ключ для всех клиентов;
    2. Сервер генерирует новый ключ для каждого отдельного клиента;

    Я думаю, каждый из Вас знает, что чем больше ключ, тем больше его практическая полезность. Однако, в случае алгоритма RSA, генерация ключа – не такая уж и простая задача, поскольку она представляет основную вычислительную сложность. К тому же, алгоритм устроен таким образом, что чем больше ключ, тем больший объем данных необходимо будет передавать.

    К примеру, при передаче сообщения длиной 5 байт, и используя ключ длиной 512 бит, зашифрованное сообщение будет «весить» 64кбайт. Это связано с тем, что максимальный объем данных, который можно зашифровать таким ключом, равен 64-11=53 кбайт (11кбайт используется для битовых сдвигов). Если необходимо зашифровать больше – разбиваем на блоки по 53 кбайт. А если взять ключ = 4096 бит, то минимальный блок будет равен 512кбайт, несмотря на то, что мы зашифровываем всего-то 5 байтов.


    Следовательно, необходимо решить:

    1. Генерировать один большой ключ для всех клиентов, который будет создавать лишний трафик, использовать больше ресурсов процессора (зашифровать сообщение ключом из 4096 бит намного сложнее, чем из 512), но кушать меньше памяти и убитых на разработку часов;
    2. Или же генерировать для каждого клиента отдельных небольшой ключ, и следить за тем, чтобы срок его использования не превышал максимально допустимого (взломать ключ длиной 512 бит уже давно стало реальностью; рекомендуемая длина – хотя бы 1024 бита);

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

    Генерация и отправка ключа серверу



    В нашем проекте используется трехуровневая архитектура: клиент-сервер-база данных. Сервер написан на Java, клиент — на C#. Ниже я буду описывать реализацию шифрования как на серверной части, так и на клиентской. Начнем именно с пользователя — клиента.

    Итак, соединение с сервером прошло успешно, и он готов принимать пакеты. Для этого мы создаем ключ, используя .NET класс RSACryptoServiceProvider (C#):

    1. private RSACryptoServiceProvider m_Rsa;
    2. private RSAParameters m_ExternKey;
    3. private RSAParameters m_InternKey;
    4.  
    5. public CryptoRsa()
    6. {
    7.     m_Rsa = new RSACryptoServiceProvider( 512 );
    8.     m_InternKey = m_Rsa.ExportParameters( true );
    9. }


    В данном листинге мы видим конструктор класса CryptoRsa, который автоматически генерирует ключ длиной 512 бит и экспортирует параметры ключей (true указывает на то, что необходимо экспортировать не только открытый, но и секретный ключ) в переменную m_InternKey.

    Далее необходимо сохранить открытый ключ в байтовом формате и отправить серверу. Для этого необходимо немного разобраться в том, из чего состоят ключи RSA. Если кратко – то они состоят из так называемых открытых и секретных экспонент и единого для обоих ключей модуля. Соответственно, открытый ключ – это открытая экспонента и модуль, закрытый ключ — закрытая экспонента и модуль. Подробнее можно прочитать здесь в главе «Алгоритм создания открытого и секретного ключей».

    Записываем открытую экспоненту в буфер вывода (C#):

    1. // Записываем длину экспоненты -> экспоненту -> модуль
    2. buf.Write( (Byte) m_InternKey.Exponent.Length );
    3. buf.Write( m_InternKey.Exponent );
    4. buf.Write( m_InternKey.Modulus );


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

    После того, как сервер принял пакет с ключом, необходимо забрать ключ из пакета и сохранить его. Смотрим (Java):

    1. // Длина экспоненты
    2. int expLength = packet.readByte();
    3.  
    4. // Получаем байты экспоненты
    5. byte[] exponent = new byte[ expLength ];
    6. System.arraycopy(packet.Bytes, packet.Offset, exponent, 0, expLength);
    7.  
    8. // Получаем байты модуля
    9. byte[] modulus = new byte[ 1 + packet.Bytes.length - (packet.Offset + expLength) ];
    10. System.arraycopy(packet.Bytes, packet.Offset + expLength, modulus, 1, modulus.length - 1 );
    11.  
    12. // Магия вуду
    13. modulus[ 0 ] =  0 ;
    14.  
    15. // Сохраняем ключ
    16. RSAPublicKeySpec rsaPubKeySpec = new RSAPublicKeySpec( new BigInteger(modulus)new BigInteger(exponent) );
    17. m_ExternPublicKey = (RSAPublicKey)KeyFactory.getInstance("RSA").generatePublic(rsaPubKeySpec);      


    Думаю, здесь не нужно особо еще комментировать код, разве что странную строку, названную мною «магией вуду» :), где мы выставляем первый байт модуля равным нулю. А дело вот в чем – по неизвестным мне причинам реализация RSA в Java требует, чтобы модуль ключа всегда начинался с нуля. Возможно, это связано с тем, чтобы иметь модуль > 0, т.к. когда я пытался сам реализовать RSA на Java с использованием больших чисел (BigInteger), при неравенстве первого байта нулю получалось отрицательное число. Данный вопрос оставляю Вам, господа Хабравчане, буду очень рад, если кто-нибудь объяснит эту особенность.

    Далее идет генерация ключа сервером. Рассмотрим следующий кусок кода (Java):

    1. // Получаем и инициализируем генератора ключей
    2. KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
    3. keyGen.initialize( Config.CRYPTO_KEY_NUM_BITS );
    4.  
    5. // Генерируем связку
    6. m_KeyPair = keyGen.genKeyPair();
    7.  
    8. // Получаем открытый и секретный ключи
    9. m_InternPublicKey = (RSAPublicKey)KeyFactory.getInstance("RSA").generatePublic(
    10. new X509EncodedKeySpec(m_KeyPair.getPublic().getEncoded()));
    11.  
    12. m_InternPrivateKey = (RSAPrivateKey)KeyFactory.getInstance("RSA").generatePrivate(
    13. new PKCS8EncodedKeySpec(m_KeyPair.getPrivate().getEncoded()));


    Думаю, тут все понятно. Хотя, конечно, если углубляться, то обязательно надо погуглить на тему таких существ, как X509 и PKCS8 (X509EncodedKeySpec и PKCS8EncodedKeySpec).

    Следующим этапом является отправка ключей серверу. Производится это практически так же, как и в случае клиента (Java):

    1. // Записываем длину экспоненты -> экспоненту -> модуль
    2. bao.write( exponent.length & 0xff ); // записываем в виде байта
    3. bao.write( exponent );
    4. bao.write( modulus );


    И наконец, получаем ключ на стороне клиента, считываем и сохраняем его (C#):

    1. Byte expLength = packet.ReadByte();
    2.  
    3. byte[] exponent = new byte[expLength];
    4. Buffer.BlockCopy(packet.Bytes, packet.Offset, exponent, 0, expLength);
    5.  
    6. byte[] modulus = new byte[packet.Bytes.Length - (packet.Offset + expLength) - 1 ];
    7. Buffer.BlockCopy(packet.Bytes, packet.Offset + expLength + 1, modulus, 0, modulus.Length);
    8.  
    9. m_ExternKey = new RSAParameters();
    10. m_ExternKey.Exponent    = exponent;
    11. m_ExternKey.Modulus     = modulus;


    Вот, собственно, и все. Теперь клиент имеет открытый ключ сервера в переменной m_ExternKey, а сервер – открытый ключ клиента в переменной m_ExternPublicKey. Осталось лишь организовать саму передачу данных. Делается это еще проще (C#):

    1. // Импортируем ключ
    2. m_Rsa.ImportParameters(m_ExternKey);
    3.  
    4. // Шифруем и пишем зашифрованные данные в буфер
    5. buffer.Write( m_Rsa.Encrypt(bytesToEncrypt, false) );


    В случае сервера немного посложнее (Java):

    1. byte[] cipherText = null;
    2. Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
    3. cipher.init(Cipher.ENCRYPT_MODE, m_ExternPublicKey);
    4. cipherText = cipher.doFinal(tempBytes);
    5.  
    6. bao.write( cipherText );


    Зашифрованное сообщение готово к отправке и расшифровке у получателя с помощью закрытого ключа. Единственное, о чем не стоит забывать — это то, что максимальный размер сообщения, которое может быть зашифровано, равно размерю ключа минус 11 байт. Поэтому при шифровке необходимо делить данные на блоки и шифровать их поочереди. Вот пример на C#:

    1. m_Rsa.ImportParameters(m_ExternKey);
    2. ByteBuffer buffer = new ByteBuffer();
    3.  
    4. int dataLength  = bytesToEncrypt.Length;
    5. int maxLength   = (m_Rsa.KeySize / 8) - 12;
    6. int iterations  = (int)Math.Ceiling( (float)bytesToEncrypt.Length / maxLength );
    7.  
    8. for (Int32 i = 0; i < iterations; i++)
    9. {
    10.     byte[] tempBytes = new byte[
    11.         (dataLength - maxLength * i > maxLength) ? maxLength :
    12.             dataLength - maxLength * i];
    13.  
    14.     Buffer.BlockCopy(bytesToEncrypt, maxLength * i, tempBytes, 0,
    15.               tempBytes.Length);
    16.  
    17.     buffer.PutEnd( m_Rsa.Encrypt(tempBytes, false) );
    18. }
    19.  
    20. return buffer.Array;


    На Java реализуете сами, там изменений — пара строк :)

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

    • +12
    • 2,1k
    • 6
    Поделиться публикацией

    Комментарии 6

      –2
      Когда будет карма, перенеси в habrahabr.ru/blogs/infosecurity/
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Занятно, только из-за экономии по трафику вы решились на генерацию уникальных закрытых ключей? Хм… Это нарушает стройную идею асимметричного шифрования.
            +8
            Извините меня, но вы страдаете полной фигнёй, если не сказать больше. Вы изобрели собственный, притом весьма некачественный велосипед. Никто так в здравом уме не делает.
            Почему бы не забить на всё, и не обернуть в обычный SSL, который за вас всё сделает хорошо и красиво?

            Но, если уж хотите свою реализацию, то нужно кардинально менять логику.
            1. Основное: данные с помощью RSA не шифруют! Вы же сами рассказали про трудоёмкость операции, как результат вы нагружаете клиента и сервера этой работой. Умные люди придумали много хороших симметричных алгоритмов: DES, AES и прочие. Которые гораздо более стойкие чем RSA и гораздо более быстрые и эффективные. Генерите на сервере/клиенте случайный ключ для AES, передавайте его зашифрованным публичным от RSA и дальше используйте именно его, а про RSA забудьте.
            2. Вы правильно посчитали что публичный ключ можно раздавать всем угодно, но вот одна маленькая проблема — у вас нет гарантии того, что вы отдаёте и получаете тот ключ, который хотите, а не попали на злобного человека-в-середине. С одной стороны, это мелочь, но вы так напираете на безопасность, что данный факт забывать совершенно не следует. Решение — передавать открытый ключ отдельным каналом, или же подписать его отдельной подписью от VeriSign, который уже есть в системе у клиента (да-да, получились банальные сертификаты). Если не хочется с этим возиться: разместите этот ключ у себя на сайте, и не передавате в сообщениях, пусть клиенты его вручную вбивают, когда 10 раз убедятся что сайт не поддельный.

            Ну вот как-то так, заранее извиняюсь, если комментарий получился несколько грубым или фамильярным :)
              0
              Все верно, и еще небольшая ремарка.

              Реализация RSACryptoServiceProvider в .NET очень неудачна. Если погуглить — можно найти десяток статей, почему этим классом пользоваться нельзя.
                +1
                Спасибо за здравую критику, и правда следует пересмотреть некоторые решения.

                По сути, в статье я пытался показать один из вариантов использования RSA для шифрования в архитектуре клиент-сервер, совершенно не претендуя на то, что данный метод является лучшим. Он просто дает представление о том, как это реализовывается :)

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое