Использование инструментов криптографии в Delphi-приложениях (исходники)Источник: Королевство Delphi Юрий Спектор
Защита информации от несанкционированного доступа и распространения играет все более важную роль в современной жизни. Задача криптографии - обеспечить эту защиту. О том, чем могут быть полезны достижения этой науки для программиста, и как их использовать, и будет посвящена данная статья. Оговорюсь, что тема, затрагиваемая в статье, весьма сложная и объемная. Приведенный здесь материал - лишь капля в море. Для тех, кого тема заинтересует, могу порекомендовать книги, например:
Основные понятияПрежде всего, введем несколько понятий и терминов, чтобы в дальнейшем их смысл не вызывал у Вас вопросов или недопонимания. Открытый текст - собственно, это и есть та информация, которую мы будем пытаться защитить от несанкционированного доступа. "Открытый текст" - это вовсе не обязательно именно текст, это также могут быть двоичные данные, программный код, и т.д. Шифры подразделяются на две группы:
Говоря об асимметричных шифрах, необходимо упомянуть еще одну замечательную возможность. Представьте, что вы зашифруете сообщение не открытым ключом получателя, а своим закрытым ключом. Расшифровать такое сообщение можно с помощью вашего открытого ключа, то есть все наоборот. Теперь получается, что расшифровать может кто угодно, а зашифровать - только вы. Кроме того, внести осмысленное изменение в сообщение (например, приписать нолик к денежной сумме) никто посторонний не сможет. Таким образом, получатель сможет аутентифицировать отправителя, т.е. он будет уверен, что отправитель сообщения именно вы и никто другой. Это называется электронно-цифровая подпись (ЭЦП) или просто цифровая подпись. Какими бы замечательными асимметричные алгоритмы ни были, у них есть один существенный недостаток: по быстродействию они уступают симметричным раз в сто и используются в основном только для шифрования небольших сообщений. Поэтому на практике чаще всего применяют схемы, сочетающие в себе все достоинства и симметричных и асимметричных алгоритмов, например:
А как же быть с цифровой подписью, ведь, как уже было сказано ранее, длинные сообщения шифровать с помощью асимметричных алгоритмов неразумно? Прежде чем ответить на этот вопрос, познакомимся с еще одним понятием - хэш-функция. Хэш-функция - это такая функция, значение которой является необратимым преобразованием исходного значения. Другими словами, пусть у нас есть число 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 году. Идея алгоритма заключается в следующем: перемножить два числа намного проще, чем разложить произведение на множители. Об этом алгоритме я расскажу поподробнее.
Сообщение зашифровывается по формуле 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, который прилагается к данной статье. Подключение к криптопровайдеру. Контейнеры ключей Первая функция, которую мы рассмотрим, будет
В большинстве случаев, работа с криптографическими возможностями Windows начинается с вызова именно этой функции, которая выполняет подключение к криптопровайдеру и возвращает его дескриптор в параметре phProv. Криптопровайдер представляет собой dll, независимый программный модуль, который фактически исполняет криптографические алгоритмы. Криптопровайдеры бывают различные и отличаются составом функций (например, некоторые криптопровайдеры ограничиваются лишь цифровыми подписями), используемыми алгоритмами (некоторые шифруют алгоритмом RC2, другие - DES) и другими возможностями. В каждой операционной системе свой состав криптопровайдеров, однако в каждой присутствует Microsoft Base Cryptographic Provider v1.0. При вызове функции CryptAcquireContext, необходимо указать имя провайдера и его тип (соответственно в параметрах pszProvider и dwProvType). Тип провайдера определяет состав функций и поддерживаемые криптоалгоритмы, например: Тип PROV_RSA_FULL
Тип PROV_RSA_SIG
Microsoft Base Cryptographic Provider v1.0 относится к типу PROV_RSA_FULL и для этого типа используется по умолчанию (если в параметре pszProvider указать nil). В параметре pszContainer необходимо указать имя контейнера ключей, который мы собираемся использовать. Дело в том, что каждый криптопровайдер содержит базу данных, в которой хранятся ключи пользователей. Эти ключи группируются в контейнерах. Сохраняются только ключевые пары для асимметричных алгоритмов, сеансовые ключи не сохраняются, так как их не рекомендуют использовать повторно. Таким образом, каждый контейнер имеет имя и содержит по одному ключу (точнее паре открытый-закрытый ключ) для цифровой подписи и обмена ключами (помните, я говорил, что из-за низкого быстродействия асимметричные алгоритмы используются в основном только для шифрования сеансовых ключей и подписи хэша). В зависимости от криптопровайдера, база данных может храниться в файлах, реестре или в каких-либо аппаратных средствах, но это не влияет на работу программиста с контейнерами ключей. Если в качестве параметра pszContainer указать nil, то будет использоваться контейнер ключей, название которого совпадает именем пользователя, под которым был осуществлен вход в систему. Но так делать не рекомендуется: дело в том, что если два приложения использует один и тот же контейнер, одно из них может изменить или уничтожить ключи, необходимые для корректной работы другого приложения. Поэтому рекомендуют использовать контейнеры, имена которых совпадает с именем приложения. Параметр dwFlags может быть нулевым или принимать одно из следующих значений:
В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.
Освобождает контекст криптопровайдера и контейнера ключей. hProv - дескриптор криптопровайдера, полученный при вызове CryptAcquireContext. dwFlags - зарезервирован и должен равняться нулю. В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки. Приведем пример работы с этими функциями:
Прежде, чем перейти непосредственно к криптографическим функциям, упомяну еще о таких функциях как CryptSetProvider, CryptGetDefaultProvider, CryptGetProvParam, CryptSetProvParam, CryptEmunProviders, CryptEnumProviderTypes, описание которых вы найдете сами. Хэширование и электронно-цифровая подпись
Функция создает в системе хэш-объект и возвращает в параметре phHash его дескриптор. Данные, поступающие на вход хэш-объекта, там преобразуются, и их отпечаток сохраняется внутри хэш-объекта. В параметре hProv нужно указать дескриптор провайдера, полученный с помощью CryptAcquireContext. Параметр Algid указывает на то, какой алгоритм хэширования будет использоваться. Для Microsoft Base Cryptographic Provider может принимать следующие значения: CALG_MAC, CALG_MD2, CALG_MD5, CALG_SHA. Смысл этих значений, думаю, понятен. Параметр hKey подробно рассматривать не будем, вы можете почитать о нем сами. Скажу лишь, что обычно (если не используется алгоритм с секретным ключом, такой как MAC) его указывают равным нулю. Параметр dwFlags зарезервирован на будущее и должен быть равен нулю. В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.
Функция уничтожает хэш-объект, созданный с помощью CryptCreateHash. В параметре hHash указывается дескриптор хэш-объекта. В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.
Функция позволяет добавлять данные к объекту хэш-функции. Функция может вызываться несколько раз, данные, от которых мы вычисляем хэш, разбиты на порции. В параметре hHash указывается дескриптор хэш-объекта, созданный с помощью CryptCreateHash. pbData содержит указатель на данные, а dwDataLen содержит размер этих данных в байтах. Для Microsoft Base Cryptographic Provider параметр dwFlags должен быть равен нулю. В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.
Функция вычисляет значение электронно-цифровой подписи от значения хэша. В параметре hHash указывается дескриптор хэш-объекта, созданный с помощью CryptCreateHash. dwKeySpec указывает, какой ключ будет использован для создания подписи. Как уже говорилось, в хранилище ключей содержится две ключевые пары: для подписи и для обмена ключами. Соответственно этот параметр может принимать значения AT_SIGNATURE или AT_KEYEXCHANGE (логичнее использовать AT_SIGNATURE). Ключи должны существовать в контейнере. sDescription может содержать произвольную строку описания. Эта строка будет добавлена к хэшу и должна быть известна приемной стороне. Использовать этот параметр не рекомендуется, так как это снижает безопасность системы. Параметр dwFlags не поддерживается в Microsoft Base Cryptographic Provider и на его месте следует указать ноль. pbSignature указывает на буфер, куда будет помещена цифровая подпись, а pdwSigLen - размер этого буфера. Если размер заранее не известен, то можно указать pbSignature равным nil, и тогда в параметре pdwSigLen мы получим необходимый размер буфера. В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки.
Функция осуществляет проверку цифровой подписи. hHash - дескриптор хэш-объекта, значение которого является отпечатком сообщения, подпись которого мы проверяем. pbSignature - указатель на буфер, содержащий подпись, dwSigLen - размер этого буфера. hPubKey - дескриптор открытого ключа, с помощью которого мы будем проверять подпись. Открытый ключ должен соответствовать закрытому, которым осуществлялась подпись. О том, как получить этот ключ, поговорим позже. Параметры sDescription и dwFlags должны соответствовать параметрам функции CryptSignHash при осуществлении подписи. В случае успеха, функция возвращает true, в противном случае - false. GetLastError вернет код ошибки. Приведу пример работы с этими функциями, упрощенно, без контроля возможных ошибок (в реальных приложениях на это не стоит закрывать глаза): 1. Создание подписи
2. Проверка подписи В коде будут упущены некоторые фрагменты, о которых мы поговорим позже.
Рекомендую ознакомиться самостоятельно с функциями CryptHashSessionKey, CryptGetHashParam и CryptSetHashParam. Шифрование на основе пользовательских данных или пароля Для шифрования в CryptoAPI используются симметричные алгоритмы, ключ для которых может быть получен двумя путями: случайным образом или на основе каких-либо пользовательских данных, например пароля. Причем к последнему варианту генерации ключа есть одно важное требование: при использовании одних и тех же паролей должны получаться идентичные ключи. Такая возможность предусмотрена в CryptoAPI.
В параметре 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.
В параметре hKey передается дескриптор ключа, необходимый для шифрования. Этот ключ также определяет алгоритм шифрования. Параметр hHash используется, если данные одновременно шифруются и хэшируются (шифроваться и хэшироваться будут исходные данные). В этом случае в параметре hHash передается дескриптор заранее созданного хэш-объекта. Эту возможность удобно использовать, если необходимо одновременно зашифровать и подписать сообщение. Иначе этот параметр следует установить в ноль. Параметр Final следует установить в true, если переданный в функцию блок данных является единственным или последним. В этом случае он будет дополнен до необходимого размера. Параметр dwFlags не используется в Microsoft Base Cryptographic Provider и на его месте следует указать ноль. pbData - указатель на буфер, в котором содержаться данные для зашифрования. Зашифрованыые данные помещаются в тот же буфер. pdwDataLen - размер данных, которые будут зашифрованы. dwBufLen - размер выходного буфера, для блочных шифров может быть больше, чем pdwDataLen. Узнать необходимый размер, можно передав в параметре pbData nil, в параметре pdwDataLen - размер данных, которые необходимо зашифровать, а в параметре dwBufLen - что угодно, например ноль. После такого вызова, необходимый размер буфера будет содержаться в параметре pdwDataLen (именно pdwDataLen, а не dwBufLen, немного нелогично, ну да ладно). Чтобы не было путаницы, приведу простой пример:
Теперь, рассмотрим функцию, которая позволяет расшифровать сообщение.
Даже не буду подробно описывать все параметры - тут все очевидно. Скажу лишь, что в параметр pdwDataLen нужно передать число байт шифротекста, а после вызова в него будет помещена длина открытого сообщения. Если используется параметр hHash, то данные после расшифровки хэшируются. Это удобно использовать, если нужно одновременно расшифровать сообщение и проверить подпись. После того, как работа с ключом закончена, необходимо освободить дескриптор:
Если hKey относится к сеансовому ключу или импортированному открытому ключу (об этом ниже), то дескриптор освобождается, а ключ уничтожается. Если hKey относится к паре открытый/закрытый ключ, то дескриптор освобождается, а ключевая пара сохраняется в контейнере ключей. В прилагаемом к статье архиве, вы найдете демонстрационный пример работы с этими функциями. Генерация случайных ключей. Импорт/экспорт ключейТолько что мы рассмотрели случай, когда для зашифровки и расшифровки сообщения отправитель и получатель использовали пароль, известный только им. Сейчас рассмотрим другой: отправитель генерирует ключ случайно и передает его получатель в зашифрованном виде вместе с сообщением. При этом для шифрования сеансового ключа используется открытый ключ получателя. А где отправитель его возьмет? Как уже было сказано, при создании ключевого контейнера с помощью функции CryptAcquireContext, ключи в контейнере не создаются, их нужно сгенерировать отдельно. Рассмотрим функцию:
Функция предназначена для генерации случайных сеансовых ключей и ключевых пар. Параметры этой функции аналогичны одноименным параметрам функции CryptDeriveKey, за исключением того, что Algid может также принимать значения AT_KEYEXCHANGE и AT_SIGNATURE. В этом случае будут сгенерированы ключевые пары соответственно для обмена ключами и цифровой подписи. Создание нового ключевого контейнера должно выглядеть примерно так:
Созданные таким образом ключевые пары, впоследствии можно извлечь из контейнера, воспользовавшись функцией
Параметр dwKeySpec может принимать два значения: AT_KEYEXCHANGE и AT_SIGNATURE, значения которых очевидны. Дескриптор ключа возвращается в параметре phUserKey. Теперь ответим на вопрос, как отправитель сможет передать получателю свою открытую часть ключа.
Функция позволяет экспортировать ключ в двоичный буфер, который впоследствии можно будет сохранить в файл и передать кому-либо. В параметре hKey должен содержаться дескриптор экспортируемого ключа. Экспортировать можно не только открытые ключи, а также ключевые пары целиком и сеансовые ключи. В последних двух случаях, ключи и ключевые пары должны быть созданы функциями CryptGenKey или CryptDeriveKey с параметрами dwFlags равными CRYPT_EXPORTABLE. Открытые же ключи всегда экспортируемы. Сеансовые ключи и ключевые пары экспортируются только в зашифрованном виде. Параметр hExpKey определяет ключ, которым они будут зашифрованы. Если экспортируется открытая часть ключа, то этот параметр следует установить в ноль, если экспортируется ключевая пара целиком, то здесь обычно передают дескриптор сеансового ключа (обычно полученный с помощью CryptDeriveKey), которым пара будет зашифрована, если экспортируется сеансовый ключ, то обычно он шифруется открытым ключом получателя (обычно используется ключ обмена, но никто не запрещает использовать ключ подписи). Параметр dwBlobType определяет тип экспортируемого ключа и может принимать следующие значения: SIMPLEBLOB - сеансовый ключ, PUBLICKEYBLOB - открытый ключ, PRIVATEKEYBLOB - ключевая пара целиком. Существуют и другие значения, но они не поддерживаются стандартным криптопровайдером. Параметр dwFlags для Microsoft Base Cryptographic Provider должен быть равен нулю. pbData - буфер, куда будут скопированы данные, pdwDataLen - размер этого буфера. Если он заранее не известен, то можно указать в качестве параметра pbData nil, и в pdwDataLen будет получен необходимый размер. Вот пример экспорта открытого ключа:
Импорт ключа осуществляется с помощью функции
Тут практически все понятно. Поясню лишь, что в параметре hPubKey необходимо передать дескриптор ключа, которым будет расшифрован импортированный ключ. Если импортируется ключевая пара целиком, то параметр dwFlags можно установить в CRYPT_EXPORTABLE, тогда импортированная пара может быть впоследствии также экспортирована. В параметре phKey вернется дескриптор полученного ключа. Если это ключевая пара, то она будет сохранена в контейнере. Вот пример импорта открытого ключа:
Теперь, воспользовавшись этой информацией, вы без труда сможете восстановить пропущенные фрагменты в функции проверки цифровой подписи, описанной ранее. Итак, как же передать собеседнику зашифрованное сообщение:
Говоря о сеансовых ключах, используемых в 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, примерно так:
При экспорте ключа солт-значение не сохраняется, о нем должен позаботиться сам программист.
Сохраненное таким образом солт-значение необходимо передать вместе с сеансовым ключом, а на приемной стороне "вживить" его туда снова.
Для работы с солт-значениями мы воспользовались функциями CryptGetKeyParam и CryptSetKeyParam, однако их возможности на этом не заканчиваются. Рекомендую ознакомиться с ними самостоятельно, а также с другими функциями, которые в данной статье не упоминались: CryptGenRandom, CryptDuplicateKey, CryptDublicateHash. Другие полезности К данной статье еще прилагаются некоторые бесплатные библиотеки и программы, которые не имеют отношения к CryptoAPI, но также работают со средствами криптографии и могут быть очень полезны:
|