Использование инструментов криптографии в Delphi-приложениях (исходники)

Юрий Спектор

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

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

  1. Брюс Шнайер - "Прикладная криптография".
  2. Щербаков Л.Ю. Домашен А.В. - "Прикладная криптография".

Основные понятия

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

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

Шифры подразделяются на две группы:

  • Симметричные - для шифрования и дешифрования используется один и тот же ключ. Очевидно, что "секретность" шифрованного текста зависит от "секретности" ключа, поэтому такие ключи так и называются "секретными". Тут есть одна проблема: при передаче сообщения собеседнику, необходимо чтобы у него был тот же ключ, что и у Вас. А где гарантия, что при передаче ключа собеседнику, его никто не перехватит? Эта проблема решается с помощью асимметричных алгоритмов шифрования. Симметричные алгоритмы могут быть блочными (сообщение разбивается на блоки фиксированной длины, каждый из которых шифруется отдельно) и потоковыми (сообщение шифруется посимвольно). При использовании блочных шифров размер сообщения должен быть кратен размеру блока, в противном случае последний блок дополняется до необходимой длины. Блочные шифры считаются более надежными.
  • Асимметричные - для шифрования и дешифрования используются разные ключи. Один из ключей держится в строжайшем секрете (он называется "закрытый"), другой - публикуется ("открытый"). Теперь представьте, что вы хотите передать какую-либо секретную информацию вашему другу или коллеге. Вы возьмете его открытый ключ (как уже было сказано - он не является секретным, и узнать его может кто угодно) и зашифруете с помощью его свое сообщение. Получив шифрованный текст, он попытается расшифровать его с помощью своего закрытого ключа. Так как закрытый ключ кроме него не известен никому, то полученное сообщение не сможет восстановить никто посторонний.

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

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

  1. Секретный ключ, с помощью которого будет зашифровано сообщение в данном сеансе связи (этот ключ не рекомендуют использовать повторно и называют "сеансовым") шифруется асимметричным алгоритмом с помощью открытого ключа получателя.
  2. Сообщение шифруется симметричным алгоритмом с помощью сеансового ключа.
  3. Зашифрованный сеансовый ключ и сообщение отправляется получателю.
  4. Получатель сначала расшифровывает сеансовый ключ с помощью своего закрытого ключа, а потом и само сообщения.

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

Хэш-функция - это такая функция, значение которой является необратимым преобразованием исходного значения. Другими словами, пусть у нас есть число A. Вычислим Y=H(A). Функция H будет необратимой, если зная значение Y восстановить A будет невозможно. Такому условию удовлетворяет, например, простейшая контрольная сумма, однако к хэш-функциям есть еще одно серьезное требование: очень сложной задачей должно являться нахождение такого числа B не равного A, что H(B) также будет равняться Y (такие случаи называются коллизиями ). Число Y называют дайджестом или отпечатком сообщения.

Где это может пригодиться? Например, хорошим решением будет хранить в базе данных паролей не сами пароли, а их отпечатки, при вводе пользователем пароля, высчитывать его отпечаток и сравнивать со значением в базе данных. Если злоумышленник получит доступ к этой базе, то пароли он узнать не сможет, так как хэш-функция необратима. Также он вряд ли сможет подобрать другой пароль с аналогичным отпечатком.

А теперь вернемся к цифровым подписям. Как уже было сказано, подписывать целое сообщение неразумно. В цифровой подписи главное не секретность самого сообщения, а гарантия того, что отправитель тот за кого себя выдает, и текст сообщения не был изменен после подписания. Обычно поступают так: высчитывается отпечаток сообщения (обычно он составляет 16-64 байт), шифруется закрытым ключом отправителя и передается вместе с самим сообщением. Получатель вычисляет отпечаток сообщения, расшифровывает подпись открытым ключом отправителя и сравнивает полученные значения. Эта процедура называется верификацией .

Популярные алгоритмы

До этого мы говорили о каких-то абстрактных алгоритмах, а теперь настало время назвать их по именам. Среди симметричных алгоритмов можно выделить алгоритм DES (разработанный фирмой IBM и утвержденный в 1977 году правительством США как официальный стандарт. Блочный алгоритм. Несмотря на популярность, алгоритм уязвим, истории известны случаи взлома), 3-DES, который на самом деле представляет собой ни что иное, как тройное шифрование DES тремя ключами, RC2 (блочный), RC4 (потоковый), IDEA (блочный). У каждого из них свои достоинства и недостатки.

Среди асимметричных алгоритмов следует выделить RSA, названный в честь Рона Ривеста, Ади Шамира и Лена Адельмана, разработавших алгоритм в 1977 году. Идея алгоритма заключается в следующем: перемножить два числа намного проще, чем разложить произведение на множители. Об этом алгоритме я расскажу поподробнее.

  1. Для начала нужно сгенерировать два больших простых числа p и q.
  2. Найти n=pq.
  3. Выбрать число e (обычно порядка 10000) взаимно простое с phi=(p-1)(q-1), т.е. числа e и phi не имеют никаких общих делителей, кроме 1.
  4. Генерируется число d такое, что ed=1(mod phi) - запись означает, что (ed-1) делится на phi.
  5. Числа n и е публикуются как открытый ключ, а число d держится в строжайшей тайне - это закрытый ключ. Числа p и q желательно либо уничтожить, либо также хранить в тайне.

Сообщение зашифровывается по формуле y=xe(mod n), где x - исходное сообщение, а y - зашифрованное. Расшифровывается с помощью закрытого ключа d следующим образом: x=yd(mod n). Надежность алгоритма заключается в том, что для восстановления закрытого ключа d необходимо знать числа p и q. Их можно получить, разложив на множители число n, но если числа p и q достаточно большие, то эта задача становится практически неразрешимой. В настоящий момент рекомендуют выбирать p и q такие, чтобы произведение n было не короче 1024 бит.

Ну, и среди алгоритмов хеширования можно назвать следующие: MD4 (128-разрядный отпечаток), MD5 (Разработан в 1991 году, 128-разрядный отпечаток, пришел на смену MD4, в 2004 году в алгоритме обнаружена уязвимость, позволяющая довольно быстро находить коллизии), SHA-1 (Разработан в 1995 году, 160-разрядный отпечаток, долгое время был наиболее популярным, однако в начале 2005 года с ним произошло то же самое, что и с MD5. Брюс Шнайер заявил: " SHA-1 has been broken"), SHA-224, SHA-256, SHA-384, SHA-512.

CryptoAPI

Криптографические функции являются частью операционной системы Windows, и обратится к ним можно посредством интерфейса CryptoAPI. Основные возможности доступны еще с Windows 95, но со временем они расширялись. Описание функций CryptoAPI можно найти в MSDN, литературе[2] или в справочном файле к Delphi. Функции содержаться в библиотеках advapi32.dll и crypt32.dll. Их можно импортировать самостоятельно, а можно воспользоваться файлом Wcrypt2.pas, который прилагается к данной статье.

Подключение к криптопровайдеру. Контейнеры ключей

Первая функция, которую мы рассмотрим, будет

function CryptAcquireContext(phProv       :PHCRYPTPROV;
                             pszContainer :LPAWSTR;
                             pszProvider  :LPAWSTR;
                             dwProvType   :DWORD;
                             dwFlags      :DWORD) :BOOL; stdcall;

В большинстве случаев, работа с криптографическими возможностями Windows начинается с вызова именно этой функции, которая выполняет подключение к криптопровайдеру и возвращает его дескриптор в параметре phProv. Криптопровайдер представляет собой dll, независимый программный модуль, который фактически исполняет криптографические алгоритмы. Криптопровайдеры бывают различные и отличаются составом функций (например, некоторые криптопровайдеры ограничиваются лишь цифровыми подписями), используемыми алгоритмами (некоторые шифруют алгоритмом RC2, другие - DES) и другими возможностями. В каждой операционной системе свой состав криптопровайдеров, однако в каждой присутствует Microsoft Base Cryptographic Provider v1.0. При вызове функции CryptAcquireContext, необходимо указать имя провайдера и его тип (соответственно в параметрах pszProvider и dwProvType). Тип провайдера определяет состав функций и поддерживаемые криптоалгоритмы, например:

Тип PROV_RSA_FULL

  • Обмен ключами - алгоритм RSA
  • Цифровая подпись - алгоритм RSA
  • Шифрование - алгоритм RC2 и RC4
  • Хэширование - алгоритмы MD5 и SHA

Тип PROV_RSA_SIG

  • Обмен ключами - не поддерживается
  • Цифровая подпись - алгоритм RSA
  • Шифрование - не поддерживается
  • Хэширование - алгоритмы MD5 и SHA

Microsoft Base Cryptographic Provider v1.0 относится к типу PROV_RSA_FULL и для этого типа используется по умолчанию (если в параметре pszProvider указать nil). В параметре pszContainer необходимо указать имя контейнера ключей, который мы собираемся использовать. Дело в том, что каждый криптопровайдер содержит базу данных, в которой хранятся ключи пользователей. Эти ключи группируются в контейнерах. Сохраняются только ключевые пары для асимметричных алгоритмов, сеансовые ключи не сохраняются, так как их не рекомендуют использовать повторно. Таким образом, каждый контейнер имеет имя и содержит по одному ключу (точнее паре открытый-закрытый ключ) для цифровой подписи и обмена ключами (помните, я говорил, что из-за низкого быстродействия асимметричные алгоритмы используются в основном только для шифрования сеансовых ключей и подписи хэша). В зависимости от криптопровайдера, база данных может храниться в файлах, реестре или в каких-либо аппаратных средствах, но это не влияет на работу программиста с контейнерами ключей. Если в качестве параметра pszContainer указать nil, то будет использоваться контейнер ключей, название которого совпадает именем пользователя, под которым был осуществлен вход в систему. Но так делать не рекомендуется: дело в том, что если два приложения использует один и тот же контейнер, одно из них может изменить или уничтожить ключи, необходимые для корректной работы другого приложения. Поэтому рекомендуют использовать контейнеры, имена которых совпадает с именем приложения.

Параметр dwFlags может быть нулевым или принимать одно из следующих значений:
CRYPT_VERIFYCONTEXT - этот флаг предназначен для приложений, которые не должны иметь доступ к закрытым ключам контейнера. Такие приложения могут обращаться только к функциям хеширования, проверки цифровой подписи или симметричного шифрования. В этом случае параметр pszContainer должен быть равен nil.
CRYPT_NEWKEYSET - создает новый контейнер ключей, но сами ключи не создаются.
CRYPT_DELETEKEYSET - удаляет контейнер вместе с хранящимися там ключами. Если задан этот флаг, то подключение к криптопровайдеру не происходит и параметр phProv неопределен.
CRYPT_MACHINE_KEYSET - по умолчанию контейнеры ключей сохраняются как пользовательские. Для основных криптопровайдеров это означает, что контейнеры ключей сохраняются в пользовательских профилях. Этот флаг можно устанавливать в комбинации с другими, чтобы указать, что контейнер является машинным, то есть хранится в профиле All Users.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

function CryptReleaseContext(hProv   :HCRYPTPROV;
                             dwFlags :DWORD) :BOOL; stdcall;

Освобождает контекст криптопровайдера и контейнера ключей. hProv - дескриптор криптопровайдера, полученный при вызове CryptAcquireContext. dwFlags - зарезервирован и должен равняться нулю.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

Приведем пример работы с этими функциями:

uses Wcrypt2;
...
procedure CryptProc;
var
  Prov: HCRYPTPROV;
begin
  CryptAcquireContext(@Prov,nil,nil,PROV_RSA_FULL,CRYPT_VERIFYCONTEXT);
  // Работаем с функциями CryptoAPI
...
  CryptReleaseContext(Prov,0);
end;

Прежде, чем перейти непосредственно к криптографическим функциям, упомяну еще о таких функциях как CryptSetProvider, CryptGetDefaultProvider, CryptGetProvParam, CryptSetProvParam, CryptEmunProviders, CryptEnumProviderTypes, описание которых вы найдете сами.

Хэширование и электронно-цифровая подпись

function CryptCreateHash(hProv   :HCRYPTPROV;
                         Algid   :ALG_ID;
                         hKey    :HCRYPTKEY;
                         dwFlags :DWORD;
                         phHash  :PHCRYPTHASH) :BOOL; stdcall;

Функция создает в системе хэш-объект и возвращает в параметре phHash его дескриптор. Данные, поступающие на вход хэш-объекта, там преобразуются, и их отпечаток сохраняется внутри хэш-объекта.

В параметре hProv нужно указать дескриптор провайдера, полученный с помощью CryptAcquireContext. Параметр Algid указывает на то, какой алгоритм хэширования будет использоваться. Для Microsoft Base Cryptographic Provider может принимать следующие значения: CALG_MAC, CALG_MD2, CALG_MD5, CALG_SHA. Смысл этих значений, думаю, понятен. Параметр hKey подробно рассматривать не будем, вы можете почитать о нем сами. Скажу лишь, что обычно (если не используется алгоритм с секретным ключом, такой как MAC) его указывают равным нулю. Параметр dwFlags зарезервирован на будущее и должен быть равен нулю.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

function CryptDestroyHash(hHash :HCRYPTHASH) :BOOL; stdcall;

Функция уничтожает хэш-объект, созданный с помощью CryptCreateHash. В параметре hHash указывается дескриптор хэш-объекта.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

function CryptHashData(hHash       :HCRYPTHASH;
                 const pbData      :PBYTE;
                       dwDataLen   :DWORD;
                       dwFlags     :DWORD) :BOOL; stdcall;

Функция позволяет добавлять данные к объекту хэш-функции. Функция может вызываться несколько раз, данные, от которых мы вычисляем хэш, разбиты на порции. В параметре hHash указывается дескриптор хэш-объекта, созданный с помощью CryptCreateHash. pbData содержит указатель на данные, а dwDataLen содержит размер этих данных в байтах. Для Microsoft Base Cryptographic Provider параметр dwFlags должен быть равен нулю.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

function CryptSignHash(hHash        :HCRYPTHASH;
                       dwKeySpec    :DWORD;
                       sDescription :LPAWSTR;
                       dwFlags      :DWORD;
                       pbSignature  :PBYTE;
                       pdwSigLen    :PDWORD) :BOOL; stdcall;

Функция вычисляет значение электронно-цифровой подписи от значения хэша. В параметре hHash указывается дескриптор хэш-объекта, созданный с помощью CryptCreateHash. dwKeySpec указывает, какой ключ будет использован для создания подписи. Как уже говорилось, в хранилище ключей содержится две ключевые пары: для подписи и для обмена ключами. Соответственно этот параметр может принимать значения AT_SIGNATURE или AT_KEYEXCHANGE (логичнее использовать AT_SIGNATURE). Ключи должны существовать в контейнере. sDescription может содержать произвольную строку описания. Эта строка будет добавлена к хэшу и должна быть известна приемной стороне. Использовать этот параметр не рекомендуется, так как это снижает безопасность системы. Параметр dwFlags не поддерживается в Microsoft Base Cryptographic Provider и на его месте следует указать ноль. pbSignature указывает на буфер, куда будет помещена цифровая подпись, а pdwSigLen - размер этого буфера. Если размер заранее не известен, то можно указать pbSignature равным nil, и тогда в параметре pdwSigLen мы получим необходимый размер буфера.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

function CryptVerifySignature(hHash        :HCRYPTHASH;
                        const pbSignature  :PBYTE;
                              dwSigLen     :DWORD;
                              hPubKey      :HCRYPTKEY;
                              sDescription :LPAWSTR;
                              dwFlags      :DWORD) :BOOL; stdcall;

Функция осуществляет проверку цифровой подписи. hHash - дескриптор хэш-объекта, значение которого является отпечатком сообщения, подпись которого мы проверяем. pbSignature - указатель на буфер, содержащий подпись, dwSigLen - размер этого буфера. hPubKey - дескриптор открытого ключа, с помощью которого мы будем проверять подпись. Открытый ключ должен соответствовать закрытому, которым осуществлялась подпись. О том, как получить этот ключ, поговорим позже. Параметры sDescription и dwFlags должны соответствовать параметрам функции CryptSignHash при осуществлении подписи.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

Приведу пример работы с этими функциями, упрощенно, без контроля возможных ошибок (в реальных приложениях на это не стоит закрывать глаза):

1. Создание подписи

uses Wcrypt2;
...
function SignMessage(Message: String): String;
var
  Prov: HCRYPTPROV;
  Hash: HCRYPTHASH;
  BufLen: DWORD;
begin
  Result:='';
  CryptAcquireContext(@Prov,nil,nil,PROV_RSA_FULL,0);
  CryptCreateHash(Prov,CALG_MD5,0,0,@Hash);
  CryptHashData(Hash,PByte(Message),Length(Message),0);
  BufLen:=0;
  CryptSignHash(Hash,AT_SIGNATURE,nil,0,nil,@BufLen);
  if BufLen>0 then begin
    SetLength(Result,BufLen);
    CryptSignHash(Hash,AT_SIGNATURE,nil,0,PByte(Result),@BufLen);
  end;
  CryptDestroyHash(Hash);
  CryptReleaseContext(Prov,0);
end;

2. Проверка подписи

В коде будут упущены некоторые фрагменты, о которых мы поговорим позже.

function VerifySign(Message, Sign: String): Boolean;
var
  Prov: HCRYPTPROV;
  Hash: HCRYPTHASH;
  PublicKey: HCRYPTKEY;
begin
  CryptAcquireContext(@Prov,nil,nil,PROV_RSA_FULL,0);
  CryptCreateHash(Prov,CALG_MD5,0,0,@Hash);
  CryptHashData(Hash,PByte(Message),Length(Message),0);
  // Здесь должен быть импорт открытого ключа для проверки подписи
...
  Result:=CryptVerifySignature(Hash,PByte(Sign),Length(Sign),
    PublicKey,nil,0);
  // Здесь должно быть уничтожение открытого ключа
...
  CryptDestroyHash(Hash);
  CryptReleaseContext(Prov,0);
end;

Рекомендую ознакомиться самостоятельно с функциями CryptHashSessionKey, CryptGetHashParam и CryptSetHashParam.

Шифрование на основе пользовательских данных или пароля

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

function CryptDeriveKey(hProv     :HCRYPTPROV;
                        Algid     :ALG_ID;
                        hBaseData :HCRYPTHASH;
                        dwFlags   :DWORD;
                        phKey     :PHCRYPTKEY) :BOOL; stdcall;

В параметре hProv нужно указать дескриптор провайдера, полученный с помощью CryptAcquireContext. Algid - идентификатор алгоритма, для которого генерируется ключ. Для Microsoft Base Cryptographic Provider может принимать следующие значения: CALG_RC2 и CALG_RC4. Пользовательские данные (пароль) предварительно хэшируются и дескриптор хэш-объекта передается в функцию в качестве параметра hBaseData. Старшие 16 бит параметра dwFlags могут содержать размер ключа в битах или быть нулевыми (в этом случае будет создан ключ с размером по умолчанию). Младшие 16 бит могут быть нулевыми или принимать следующие значения или их комбинации: CRYPT_EXPORTABLE, CRYPT_CREATE_SALT, CRYPT_USER_PROTECTED, CRYPT_UPDATE_KEY. К первым двум мы еще вернемся, а со смыслом остальных вы можете ознакомиться самостоятельно. В параметре phKey возвращается дескриптор созданного ключа.

В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.

Когда ключ есть, можно приступать непосредственно к шифрованию. Для этого нам понадобятся функции CryptEncrypt и CryptDecrypt.

function CryptEncrypt(hKey       :HCRYPTKEY;
                      hHash      :HCRYPTHASH;
                      Final      :BOOL;
                      dwFlags    :DWORD;
                      pbData     :PBYTE;
                      pdwDataLen :PDWORD;
                      dwBufLen   :DWORD) :BOOL; stdcall;

В параметре hKey передается дескриптор ключа, необходимый для шифрования. Этот ключ также определяет алгоритм шифрования. Параметр hHash используется, если данные одновременно шифруются и хэшируются (шифроваться и хэшироваться будут исходные данные). В этом случае в параметре hHash передается дескриптор заранее созданного хэш-объекта. Эту возможность удобно использовать, если необходимо одновременно зашифровать и подписать сообщение. Иначе этот параметр следует установить в ноль. Параметр Final следует установить в true, если переданный в функцию блок данных является единственным или последним. В этом случае он будет дополнен до необходимого размера. Параметр dwFlags не используется в Microsoft Base Cryptographic Provider и на его месте следует указать ноль. pbData - указатель на буфер, в котором содержаться данные для зашифрования. Зашифрованыые данные помещаются в тот же буфер. pdwDataLen - размер данных, которые будут зашифрованы. dwBufLen - размер выходного буфера, для блочных шифров может быть больше, чем pdwDataLen. Узнать необходимый размер, можно передав в параметре pbData nil, в параметре pdwDataLen - размер данных, которые необходимо зашифровать, а в параметре dwBufLen - что угодно, например ноль. После такого вызова, необходимый размер буфера будет содержаться в параметре pdwDataLen (именно pdwDataLen, а не dwBufLen, немного нелогично, ну да ладно). Чтобы не было путаницы, приведу простой пример:

var
  Message: String;
  BufLen, DataLen: DWORD;
...
begin
...
  Message:='Hello World!';
  BufLen:=Length(Message);
  DataLen:=Length(Message);
  // Вычисляем необходимый размер выходного буфера
  CryptEncrypt(Key,0,true,0,nil,@BufLen,0);
  // Выделяем память для буфера и шифруем
  SetLength(Message,BufLen);
  CryptEncrypt(Key,0,true,0,PByte(Message),@DataLen,BufLen);

Теперь, рассмотрим функцию, которая позволяет расшифровать сообщение.

function CryptDecrypt(hKey       :HCRYPTKEY;
                      hHash      :HCRYPTHASH;
                      Final      :BOOL;
                      dwFlags    :DWORD;
                      pbData     :PBYTE;
                      pdwDataLen :PDWORD) :BOOL; stdcall;

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

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

function CryptDestroyKey(hKey  :HCRYPTKEY) :BOOL; stdcall;

Если hKey относится к сеансовому ключу или импортированному открытому ключу (об этом ниже), то дескриптор освобождается, а ключ уничтожается. Если hKey относится к паре открытый/закрытый ключ, то дескриптор освобождается, а ключевая пара сохраняется в контейнере ключей.

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

Генерация случайных ключей. Импорт/экспорт ключей

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

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

function CryptGenKey(hProv   :HCRYPTPROV;
                     Algid   :ALG_ID;
                     dwFlags :DWORD;
                     phKey   :PHCRYPTKEY) :BOOL; stdcall;

Функция предназначена для генерации случайных сеансовых ключей и ключевых пар. Параметры этой функции аналогичны одноименным параметрам функции CryptDeriveKey, за исключением того, что Algid может также принимать значения AT_KEYEXCHANGE и AT_SIGNATURE. В этом случае будут сгенерированы ключевые пары соответственно для обмена ключами и цифровой подписи. Создание нового ключевого контейнера должно выглядеть примерно так:

uses Wcrypt2;
...
var
  Prov: HCRYPTPROV;
  ExchangeKey, SignKey: HCRYPTKEY;
begin
  CryptAcquireContext(@Prov,'My_Container',nil,PROV_RSA_FULL,CRYPT_NEWKEYSET);
  // Создаем ключевые пары
  CryptGenKey(Prov,AT_KEYEXCHANGE,0,@ExchangeKey);
  CryptGenKey(Prov,AT_SIGNATURE,0,@SignKey);
  // Работаем с функциями CryptoAPI
...
  // Освобождаем дескрипторы ключевых пар. Сами ключи сохраняются в контейнере
  CryptDestroyKey(SignKey);
  CryptDestroyKey(ExchangeKey);
  CryptReleaseContext(Prov,0);
end;

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

function CryptGetUserKey(hProv     :HCRYPTPROV;
                         dwKeySpec :DWORD;
                         phUserKey :PHCRYPTKEY) :BOOL; stdcall;

Параметр dwKeySpec может принимать два значения: AT_KEYEXCHANGE и AT_SIGNATURE, значения которых очевидны. Дескриптор ключа возвращается в параметре phUserKey.

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

function CryptExportKey(hKey       :HCRYPTKEY;
                        hExpKey    :HCRYPTKEY;
                        dwBlobType :DWORD;
                        dwFlags    :DWORD;
                        pbData     :PBYTE;
                        pdwDataLen :PDWORD) :BOOL; stdcall;

Функция позволяет экспортировать ключ в двоичный буфер, который впоследствии можно будет сохранить в файл и передать кому-либо. В параметре hKey должен содержаться дескриптор экспортируемого ключа. Экспортировать можно не только открытые ключи, а также ключевые пары целиком и сеансовые ключи. В последних двух случаях, ключи и ключевые пары должны быть созданы функциями CryptGenKey или CryptDeriveKey с параметрами dwFlags равными CRYPT_EXPORTABLE. Открытые же ключи всегда экспортируемы. Сеансовые ключи и ключевые пары экспортируются только в зашифрованном виде. Параметр hExpKey определяет ключ, которым они будут зашифрованы. Если экспортируется открытая часть ключа, то этот параметр следует установить в ноль, если экспортируется ключевая пара целиком, то здесь обычно передают дескриптор сеансового ключа (обычно полученный с помощью CryptDeriveKey), которым пара будет зашифрована, если экспортируется сеансовый ключ, то обычно он шифруется открытым ключом получателя (обычно используется ключ обмена, но никто не запрещает использовать ключ подписи). Параметр dwBlobType определяет тип экспортируемого ключа и может принимать следующие значения: SIMPLEBLOB - сеансовый ключ, PUBLICKEYBLOB - открытый ключ, PRIVATEKEYBLOB - ключевая пара целиком. Существуют и другие значения, но они не поддерживаются стандартным криптопровайдером. Параметр dwFlags для Microsoft Base Cryptographic Provider должен быть равен нулю. pbData - буфер, куда будут скопированы данные, pdwDataLen - размер этого буфера. Если он заранее не известен, то можно указать в качестве параметра pbData nil, и в pdwDataLen будет получен необходимый размер.

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

procedure ExportPublicKey(FileName: TFileName);
var
  Prov: HCRYPTPROV;
  SignKey: HCRYPTKEY;
  Stream: TMemoryStream;
  BufSize: DWORD;
begin
  CryptAcquireContext(@Prov,'My_Container',nil,PROV_RSA_FULL,0);
  CryptGetUserKey(Prov,AT_SIGNATURE,@SignKey);
  Stream:=TMemoryStream.Create;
  CryptExportKey(SignKey,0,PUBLICKEYBLOB,0,nil,@BufSize);
  Stream.SetSize(BufSize);
  CryptExportKey(SignKey,0,PUBLICKEYBLOB,0,PByte(Stream.Memory),@BufSize);
  Stream.SaveToFile(FileName);
  Stream.Free;
  CryptDestroyKey(SignKey);
  CryptReleaseContext(Prov,0);
end;

Импорт ключа осуществляется с помощью функции

function CryptImportKey(hProv     :HCRYPTPROV;
                        pbData    :PBYTE;
                        dwDataLen :DWORD;
                        hPubKey   :HCRYPTKEY;
                        dwFlags   :DWORD;
                        phKey     :PHCRYPTKEY) :BOOL; stdcall;

Тут практически все понятно. Поясню лишь, что в параметре hPubKey необходимо передать дескриптор ключа, которым будет расшифрован импортированный ключ. Если импортируется ключевая пара целиком, то параметр dwFlags можно установить в CRYPT_EXPORTABLE, тогда импортированная пара может быть впоследствии также экспортирована. В параметре phKey вернется дескриптор полученного ключа. Если это ключевая пара, то она будет сохранена в контейнере.

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

function ImportPublicKey(FileName: TFileName): HCRYPTKEY;
var
  Prov: HCRYPTPROV;
  Stream: TMemoryStream;
begin
  Stream:=TMemoryStream.Create;
  Stream.LoadFromFile(FileName);
  CryptImportKey(Prov,PByte(Stream.Memory),Stream.Size,0,0,@Result);
  Stream.Free;
end;

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

Итак, как же передать собеседнику зашифрованное сообщение:

  1. Получатель экспортирует свой открытый ключ обмена в файл и передает его отправителю сообщения.
  2. Отправитель генерирует случайный сеансовый ключ и шифрует им сообщение.
  3. Отправитель импортирует открытый ключ обмена получателя, экспортирует сеансовый ключ, шифруя его полученным ключом обмена (ключ обмена в параметре hExpKey).
  4. Зашифрованное сообщение передается вместе с зашифрованным сеансовым ключом - так называемый цифровой конверт.
  5. Получатель импортирует сеансовый ключ, расшифровывая его своим закрытым ключом обмена (его можно получить, вызвав CryptGetUserKey) и с помощью сеансового ключа расшифровывает сообщение.

Говоря о сеансовых ключах, используемых в Microsoft Base Cryptographic Provider нужно упомянуть об одной неприятности: до начала 2000 года действовал запрет на экспорт программного обеспечения, использующего средства "сильной криптографии" за пределами США и Канады. По этой причине в базовом криптопровайдере не поддерживаются ключи для симметричных алгоритмов длиной более 40 бит. Ключи длиной 56 бит разрешалось использовать только заграничным отделениям американских компаний. Для алгоритмов RC2 и RC4 рекомендуемая длина ключа должна составлять 128 бит, поэтому недостающее количество бит заполняется нулями либо случайными значениями, которые должны передаваться открыто. Надежность защиты из-за этого, разумеется, сильно страдает. В состав Windows XP входит Microsoft Enhanced Cryptographic Provider, в котором этой проблемы нет, но при использовании базового криптопровайдера, необходимо дополнять ключ до нужной длины, используя т.н. солт-значения (salt-values). Сгенерировать salt-value и внести его в ключ можно несколькими способами, но самый простой и очевидный - при вызове CryptGenKey или CryptDeriveKey передать в параметре dwFlags значение CRYPT_CREATE_SALT, примерно так:

CryptGenKey(Prov,CALG_RC2,CRYPT_EXPORTABLE or CRYPT_CREATE_SALT,@Key);

При экспорте ключа солт-значение не сохраняется, о нем должен позаботиться сам программист.

var
  SaltLen: DWORD;
  Stream: TMemoryStream;
...
begin
...
  // Определяем размер буфера для солт-значения
  CryptGetKeyParam(Key,KP_SALT,nil,@SaltLen,0);
  // Сохраняем его в файл
  Stream:=TMemoryStream.Create;
  Stream.SetSize(SaltLen);
  CryptGetKeyParam(Key,KP_SALT,PByte(Stream.Memory),@SaltLen,0);
  Stream.SaveToFile('Salt.dat');
  Stream.Free;
...

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

var
  Stream: TMemoryStream;
...
begin
...
  Stream:=TMemoryStream.Create;
  Stream.LoadFromFile('Salt.dat');
  CryptSetKeyParam(Key,KP_SALT,PByte(Stream.Memory),Stream.Size);
  Stream.Free;
...

Для работы с солт-значениями мы воспользовались функциями CryptGetKeyParam и CryptSetKeyParam, однако их возможности на этом не заканчиваются. Рекомендую ознакомиться с ними самостоятельно, а также с другими функциями, которые в данной статье не упоминались: CryptGenRandom, CryptDuplicateKey, CryptDublicateHash.

Другие полезности

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

  1. HashLib! 1.03 (C) Alex Demchenko, 2002, Moldova, Chishinev - очень хорошая и удобная библиотека, в которой реализовано множество алгоритмов хэширования: MD4, MD5, CRC32, HAVAL-128, SHA-1, SHA-256, TIGER-128, GOST, RIPEMD-128 и другие.
  2. FGInt copyright 2000, Walied Othman - отличная библиотека для работы с гигантскими целыми числами, необходимыми для работы алгоритма RSA и с самим RSA.
  3. DCPCrypt Copyright (c) 1999-2003 David Barton - огромная библиотека компонент, для работы с криптографическими функциями.
  4. RSATool2v17 - Генератор чисел p, q, e, n, d для алгоритма RSA.

Страница сайта http://185.71.96.61
Оригинал находится по адресу http://185.71.96.61/home.asp?artId=5143