Как реализовать методы криптографической защиты информации – Delphi Sources FAQ

Знакомство с криптопровайдерами

Функции CryptoAPI обеспечивают прикладным программам доступ к криптографическим возможностям Windows. Однако они являются лишь «передаточным звеном» в сложной цепи обработки информации. Основную работу выполняют скрытые от глаз программиста функции, входящие в специализированные программные (или программно-аппаратные) модули – провайдеры (поставщики) криптографических услуг (CSP – Cryptographic Service Providers), или криптопровайдеры (рис. 1).

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

Криптопровайдеры отличаются друг от друга:

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

По составу функций и обеспечивающих их алгоритмов криптопровайдеры подразделяются на типы. Например, любой CSP типа PROV_RSA_FULL поддерживает как шифрование, так и цифровые подписи, использует для обмена ключами и создания подписей алгоритм RSA, для шифрования – алгоритмы RC2 и RC4, а для хеширования – MD5 и SHA.

В зависимости от версии операционной системы состав установленных криптопровайдеров может существенно изменяться. Однако на любом компьютере с Windows можно найти Microsoft Base Cryptographic Provider, относящийся к уже известному нам типу PROV_RSA_FULL. Именно с этим провайдером по умолчанию будут взаимодействовать все программы.

Использование криптографических возможностей Windows напоминает работу программы с графическим устройством. Криптопровайдер подобен графическому драйверу: он может обеспечивать взаимодействие программного обеспечения с оборудованием (устройство чтения смарт-карт, аппаратные датчики случайных чисел и пр.).

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

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

  • CryptEnumProviders (i, резерв, флаги, тип, имя, длина_имени) – возвращает имя и тип i-го по порядку криптопровайдера в системе (нумерация начинается с нуля);
  • CryptAcquireContext (провайдер, контейнер, имя, тип, флаги) – выполняет подключение к криптопровайдеру с заданным типом и именем и возвращает его дескриптор (контекст). При подключении мы будем передавать функции флаг CRYPT_VERIFYCONTEXT, служащий для получения контекста без подключения к контейнеру ключей;
  • CryptGetProvParam (провайдер, параметр, данные, размер_данных, флаги) – возвращает значение указанного параметра провайдера, например, версии (второй параметр при вызове функции – PP_VERSION), типа реализации (программный, аппаратный, смешанный – PP_IMPTYPE), поддерживаемых алгоритмов (PP_ENUMALGS). Список поддерживаемых алгоритмов при помощи этой функции может быть получен следующим образом: при одном вызове функции возвращается информация об одном алгоритме; при первом вызове функции следует передать значение флага CRYPT_FIRST, а при последующих флаг должен быть равен 0;
  • CryptReleaseContext (провайдер, флаги) – освобождает дескриптор криптопровайдера.

Каждая из этих функций, как и большинство других функций CryptoAPI, возвращает логическое значение, равное true, в случае успешного завершения, и false – если возникли ошибки. Код ошибки может быть получен при помощи функции GetLastError. Возможные значения кодов ошибки приведены в упоминавшейся выше документации.

if not CryptGetProvParam(hProv, PP_VERSION, (@vers), @DataLen, 0)
then
begin
case int64(GetLastError) of
ERROR_INVALID_HANDLE: err := ‘ERROR_INVALID_HANDLE’;
ERROR_INVALID_PARAMETER: err := ‘ERROR_INVALID_PARAMETER’;
ERROR_MORE_DATA: err := ‘ERROR_MORE_DATA’;
ERROR_NO_MORE_ITEMS: err := ‘ERROR_NO_MORE_ITEMS’;
NTE_BAD_FLAGS: err := ‘NTE_BAD_FLAGS’;
NTE_BAD_TYPE: err := ‘NTE_BAD_TYPE’;
NTE_BAD_UID: err := ‘NTE_BAD_UID’;
else err := ‘Unknown error’;
end;
MessageDlg(‘Error of CryptGetProvParam: ‘ err, mtError, [mbOK], 0);
exit
end;

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

type algInfo = record
algID: ALG_ID;
dwBits: DWORD;
dwNameLen: DWORD;
szName: array[0..100] of char;
end;
{вспомогательная функция, преобразующая тип провайдера в строку}
function ProvTypeToStr(provType: DWORD): string;
begin
case provType of
PROV_RSA_FULL: ProvTypeToStr := ‘RSA full provider’;
PROV_RSA_SIG: ProvTypeToStr := ‘RSA signature provider’;
PROV_DSS: ProvTypeToStr := ‘DSS provider’;
PROV_DSS_DH: ProvTypeToStr := ‘DSS and Diffie-Hellman provider’;
PROV_FORTEZZA: ProvTypeToStr := ‘Fortezza provider’;
PROV_MS_EXCHANGE: ProvTypeToStr := ‘MS Exchange provider’;
PROV_RSA_SCHANNEL: ProvTypeToStr := ‘RSA secure channel provider’;
PROV_SSL: ProvTypeToStr := ‘SSL provider’;
else ProvTypeToStr := ‘Unknown provider’;
end;
end;
{вспомогательная функция, преобразующая тип реализации в строку}
function ImpTypeToStr(it: DWORD): string;
begin
case it of
CRYPT_IMPL_HARDWARE: ImpTypeToStr := ‘аппаратный’;
CRYPT_IMPL_SOFTWARE: ImpTypeToStr := ‘программный’;
CRYPT_IMPL_MIXED: ImpTypeToStr := ‘смешанный’;
CRYPT_IMPL_UNKNOWN: ImpTypeToStr := ‘неизвестен’;
else ImpTypeToStr := ‘неверное значение’;
end;
end;
{процедура вывода информации о криптопровайдерах}
procedure TMainForm.InfoItemClick(Sender: TObject);
var i: DWORD;
dwProvType, cbName, DataLen: DWORD;
provName: array[0..200] of char;
vers: array[0..3] of byte;
impType: DWORD;
ai: algInfo;
err: string;
begin
i:= 0;
FileMemo.Clear;
while (CryptEnumProviders(i, nil, 0, {проверяем наличие еще одного}
@dwProvType, nil, @cbName)) do
begin
if CryptEnumProviders(i, nil, 0, {получаем имя CSP}
@dwProvType, @provName, @cbName) then
begin
FileMemo.Lines.Add(‘Криптопровайдер: ‘ provName);
FileMemo.Lines.Add(‘Тип: ‘ IntToStr(dwProvType) ‘ – ‘
ProvTypeToStr(dwProvType));
if not CryptAcquireContext(@hProv, nil, provName, dwProvType,
CRYPT_VERIFYCONTEXT)
then
begin
{обработка ошибок}
end;
DataLen := 4;
if not CryptGetProvParam(hProv, PP_VERSION, (@vers), @DataLen, 0)
then
begin
{обработка ошибок}
end;
FileMemo.Lines.Add(‘Версия: ‘ chr(vers[1] ) ‘.’ chr(vers[0] ));
if not CryptGetProvParam(hProv, PP_IMPTYPE, @impType, @DataLen, 0)
then
begin
{обработка ошибок}
end;
FileMemo.Lines.Add(‘Тип реализации: ‘ ImpTypeToStr(impType));
FileMemo.Lines.Add(‘Поддерживает алгоритмы:’);
DataLen := sizeof(ai);
if not CryptGetProvParam(hProv, PP_ENUMALGS, @ai, @DataLen, CRYPT_FIRST)
then
begin
{обработка ошибок}
end;
with ai do
FileMemo.Lines.Add(szName #9 ‘длина ключа – ‘ IntToStr(dwBits)
‘ бит’ #9 ‘ID: ‘ IntToStr(AlgID));
DataLen := sizeof(ai);
while CryptGetProvParam(hProv, PP_ENUMALGS, @ai, @DataLen, 0) do
begin
with ai do FileMemo.Lines.Add(szName #9 ‘длина ключа – ‘
IntToStr(dwBits) ‘ бит’ #9 ‘ID: ‘ IntToStr(AlgID));
DataLen := sizeof(ai);
end;
FileMemo.Lines.Add(”);
CryptReleaseContext(hProv, 0);
end;
inc(i);
end;
end;

На рис. 2 показан пример отчета, выдаваемого приведенным выше кодом, выполненным в среде Windows 98.

Как реализовать методы криптографической защиты информации – delphi sources faq


Автор: Павел

Продолжим рассказ (см. прошлые выпуски рассылки или на сайте www.realcoding.net)о криптографических возможностях Windows. В арсенале защиты должны быть не только методы, обеспечивающие секретность передачи информации (о них мы говорили в первой части статьи). Не менее важными инструментами безопасности являются процедуры, позволяющие убедиться в целости и аутентичности данных. Кроме того, необходимо решать проблемы безопасного хранения и распределения ключей.

Проблема распределения ключей

В прошлый раз при помощи CryptoAPI мы решали такую “классическую” задачу как шифрование на основе пароля. Напомним, что пароль использовался для создания ключа шифрования какого-либо симметричного алгоритма. В таком случае расшифровать файл может лишь тот, кто знает пароль. А значит, для обеспечения конфиденциальности нужно держать пароль в строжайшем секрете – желательно, чтобы его знали лишь отправитель и получатель информации. (А еще лучше, если отправитель и получатель – одно и то же лицо.)

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

Читайте также:  Failed to sign hash ошибка исполнения функции 0x8007065b что за ошибка

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

Спасительный способ, позволяющий шифровать сообщения, обмениваясь ключами по открытым каналам связи, был придуман в середине 70-х годов прошлого столетия, а в начале восьмидесятых появился первый реализующий его алгоритм – RSA. Теперь пользователь может сгенерировать два связанных между собой ключа – ключевую пару. Один из этих ключей по несекретным каналам рассылается всем, с кем пользователь хотел бы обмениваться конфиденциальными сообщениями . Этот ключ называют открытым (англ. public key). Зная открытый ключ пользователя, можно зашифровать адресованное ему сообщение, но вот расшифровать его позволяет лишь вторая часть ключевой пары – закрытый ключ (private key). При этом открытый ключ не дает “практической” возможности вычислить закрытый: такая задача, хоть и разрешима в принципе, но при достаточно большом размере ключа требует многих лет машинного времени. Для сохранения конфиденциальности получателю необходимо лишь хранить в строгом секрете свой закрытый ключ, а отправителю – убедиться, что имеющийся у него открытый ключ действительно принадлежит адресату.

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

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

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

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

Описанная схема реализована и в CryptoAPI.

Целостность и аутентичность информации

Как удостовериться в том, что пришедшее сообщение действительно отправлено тем, чье имя стоит в графе “отправитель”? Асимметричные схемы шифрования дают нам элегантный способ аутентификации. Если отправитель зашифрует сообщение своим закрытым ключом, то успешное расшифровывание убедит получателя в том, что послать корреспонденцию мог только хозяин ключевой пары, и никто иной (рис. 3). При этом расшифровку может выполнить любой, кто имеет открытый ключ отправителя. Ведь наша цель – не конфиденциальность, а аутентификация.

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

аутентичность сообщения – создать подпись на основе закрытого ключа мог только его хозяин;

целостность данных – легко вычислить хеш-значение полученного сообщения и сравнить его с тем, которое хранится в подписи: если значения совпадают, значит, сообщение не было изменено злоумышленником после того, как отправитель его подписал.

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

Контейнеры ключей

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

Подключение к контейнеру производится одновременно с получением контекста криптопровайдера при вызове функции CryptAcquireContext – имя контейнера ключей передается функции вторым ее аргументом. Если второй аргумент содержит пустой указатель (nil), то используется имя по умолчанию, т. е. имя пользователя. В том случае, если доступ к контейнеру не нужен, можно передать в последнем аргументе функции флаг CRYPT_VERIFYCONTEXT; при необходимости создать новый контейнер используется флаг CRYPT_NEWKEYSET; а для удаления существующего контейнера вместе с хранящимися в нем ключами – CRYPT_DELETEKEYSET.

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

Создание ключевых пар

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

провайдер – дескриптор криптопровайдера, полученный в результате обращения к функции CryptAcquireContext;

алгоритм – указывает, какому алгоритму шифрования будет соответствовать создаваемый ключ. Информация об алгоритме, таким образом, является частью описания ключа. Каждый криптопровайдер использует для обмена ключами и подписи строго определенные алгоритмы. Так, провайдеры типа PROV_RSA_FULL, к которым относится и Microsoft Base Cryptographic Provider, реализуют алгоритм RSA. Но при генерации ключей знать это не обязательно: достаточно указать, какой ключ мы собираемся создать – обмена ключами или подписи. Для этого используются мнемонические константы AT_KEYEXCHANGE и AT_SIGNATURE;

флаги – при создании асимметричных ключей управляет их размером. Используемый нами криптопровайдер позволяет генерировать ключ обмена ключами длиной от 384 до 512 бит**, а ключ подписи – от 512 до 16384 бит. Чем больше длина ключа, тем выше его надежность, поэтому трудно найти причины для использования ключа обмена ключами длиной менее 512 бит, а длину ключа подписи не рекомендуется делать меньше 1024 бит**. По умолчанию криптопровайдер создает оба ключа длиной 512 бит. Необходимую длину ключа можно передать в старшем слове параметра флаги;

ключ – в случае успешного завершения функции в этот параметр заносится дескриптор созданного ключа.

Рассмотрим пример создания ключевых пар при помощи формы, показанной на рис. 5. В поле “Контейнер” можно указать имя контейнера ключей; если оставить это поле пустым, будет использован контейнер по умолчанию. Назначение остальных элементов управления должно быть интуитивно понятным. После генерации ключа в memo-поле выводится отчет о его параметрах. Для этого используется функция CryptGetKeyParam (ключ, параметр, буфер, размер, флаги). Чтобы получить информацию о требуемом параметре, нужно через второй аргумент функции передать соответствующую константу: KP_ALGID – идентификатор алгоритма, KP_KEYLEN – размер ключа, и т. д. Ниже приведен текст процедуры генерации ключей без операторов обработки ошибок:

procedure TGenerateForm.OKBtnClick(Sender: TObject);
var
  cont: PChar;
  err: string;
  hProv: HCRYPTPROV;
  KeyExchKey, SignKey: HCRYPTKEY;
  flag, keyLen: DWORD;
begin{если ни один ключ не выбран - выход}ifnot (KEKCheckBox.Checked or SKCheckBox.Checked) then
    exit;
  {"считываем" имя контейнера}if length(ContainerEdit.Text) = 0 then
    cont := nilelsebegin
    err := ContainerEdit.Text;
    cont := StrAlloc(length(err)   1);
    StrPCopy(cont, err);
  end;
  CryptAcquireContext(@hProv, cont, nil, PROV_RSA_FULL, 0);
  {генерация ключа обмена ключами (Key Exchange Key)}if KEKCheckBox.Checked thenbegin{"считываем" длину ключа и помещаем ее в
    старшее слово параметра ФЛАГИ}
    keyLen := strtoint(KeyExchLenEdit.text);
    flag := keyLen shl 16;
    ifnot CryptGenKey(hProv, AT_KEYEXCHANGE, flag, @KeyExchKey) thenbegin
      ?обработка ошибок?
    endelsebegin
      ReportMemo.Lines.Add('');
      ReportMemo.Lines.Add('Создан ключ обмена ключами:');
      flag := 4;
      ifnot CryptGetKeyParam(KeyExchKey, KP_KEYLEN, @keyLen, @flag, 0) thenbegin
        ?обработка ошибок?
      endelse
        ReportMemo.Lines.Add(' длина ключа - '   inttostr(keyLen));
      flag := 4;
      ifnot CryptGetKeyParam(KeyExchKey, KP_ALGID, @keyLen, @flag, 0) thenbegin
        ?обработка ошибок?
      endelse
        ReportMemo.Lines.Add(' алгоритм - '   algIDtostr(keyLen));
      {функция algIDtostr здесь не приводится. Она состоит из единственного
      оператора case, отображающего целый идентификатор алгоритма в строку}end;
  end;
  {генерация ключа подписи (Signature Key)}if SKCheckBox.Checked thenbegin
    ?выполняется аналогично генерации ключа обмена ключами?
  end;
  CryptReleaseContext(hProv, 0);
end;

Обмен ключами

Читайте также:  Электронный ключ: особенности получения и использования

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

  • PUBLICKEYBLOB – используется для сохранения открытых ключей. Поскольку открытые ключи не являются секретными, они сохраняются в незашифрованном виде;
  • PRIVATEKEYBLOB – используется для сохранения ключевой пары целиком (открытого и закрытого ключей). Эти данные являются в высшей степени секретными, поэтому сохраняются в зашифрованном виде, причем для шифрования используется сеансовый ключ (и, соответственно, симметричный алгоритм);
  • SIMPLEBLOB – используется для сохранения сеансовых ключей. Для обеспечения секретности данные ключа шифруются с использованием открытого ключа получателя сообщения.

Экспорт ключей в CryptoAPI выполняется функцией CryptExportKey (экспортируемый ключ, ключ адресата, формат, флаги, буфер, размер буфера):

экспортируемый ключ – дескриптор нужного ключа;

ключ адресата – в случае сохранения открытого ключа должен быть равен нулю (данные не шифруются);

  • формат – указывается один из возможных форматов экспорта (PUBLICKEYBLOB, PRIVATEKEYBLOB, SIMPLEBLOB);
  • флаги – зарезервирован на будущее (должен быть равен нулю);
  • буфер – содержит адрес буфера, в который будет записан ключевой BLOB (Binary Large OBject – большой двоичный объект);
  • размер буфера – при вызове функции в этой переменной должен находиться доступный размер буфера, а по окончании работы в нее записывается количество экспортируемых данных. Если размер буфера заранее не известен, то функцию нужно вызвать с параметром буфер, равным пустому указателю, тогда размер буфера будет вычислен и занесен в переменную размер буфера.

Экспорт ключевой пары целиком, включая и закрытый ключ, может понадобиться для того, чтобы иметь возможность подписывать документы на различных компьютерах (например, дома и на работе), или для сохранения страховочной копии. В этом случае нужно создать ключ шифрования на основании пароля (см. “К П”, ? 10/2002) и передать дескриптор этого ключа в качестве второго параметра функции CryptExportKey.

Запросить у криптопровайдера дескриптор самого’ экспортируемого ключа позволяет функция CryptGetUserKey (провайдер, описание ключа, дескриптор ключа). Описание ключа – это либо AT_KEYEXCHANGE, либо AT_SIGNATURE.

Ниже приведены наиболее важные фрагменты программы:

procedure TExportForm.OKBtnClick(Sender: TObject);
var
  cont: PChar;
  err: string;
  hProv: HCRYPTPROV;
  key, expKey: HCRYPTKEY;
  pbuf: PBYTE;
  buflen: DWORD;
  f: file;
  hash: HCRYPTHASH;
begin{если ни один ключ не выбран - выход}ifnot (KEKCheckBox.Checked or SKCheckBox.Checked) then
    exit;
  {если нужен пароль, т.е. экспортируется ключевая пара целиком}if PasswEdit.Enabled and (PasswEdit.Text <> Passw2Edit.Text) thenbegin
    MessageDlg('Ошибка при вводе пароля! Повторите ввод.', mtError, [mbOK], 0);
    exit;
  end;
  :
  "считываем" имя контейнера и подключаемся к криптопровайдеру
    :
  если нужен ключ шифрования - создаем его на основании пароля
    :
  {ключ обмена ключами}if KEKCheckBox.Checked thenrepeat{получаем дескриптор ключа}
      CryptGetUserKey(hProv, AT_KEYEXCHANGE, @key);
      {пытаемся определить размер буфера для экспорта ключа}if (WhatRadioGroup.ItemIndex = 0) then
        CryptExportKey(key, 0, PUBLICKEYBLOB, 0, nil, @bufLen)
      else
        CryptExportKey(key, expKey, PRIVATEKEYBLOB, 0, nil, @bufLen);
      GetMem(pbuf, bufLen);
      {экспортируем данные}if (WhatRadioGroup.ItemIndex = 0) then
        CryptExportKey(key, 0, PUBLICKEYBLOB, 0, pbuf, @bufLen)
      else
        CryptExportKey(key, expKey, PRIVATEKEYBLOB, 0, pbuf, @bufLen);
      {освобождаем дескриптор ключа обмена ключами
      (сам ключ при этом не уничтожается)}
      CryptDestroyKey(key);
      SaveDialog1.Title := 'Укажите файл для сохранения ключа обмена ключами';
      if SaveDialog1.Execute thenbegin
        AssignFile(f, SaveDialog1.FileName);
        rewrite(f, 1);
        BlockWrite(f, pbuf^, bufLen);
        CloseFile(f);
        MessageDlg('Ключ обмена ключами успешно сохранен', mtInformation,
          [mbOK], 0);
      end;
    until true; {KeyExchange}{ключ подписи}if SKCheckBox.Checked thenrepeat
      ?аналогично ключу обмена ключами?
    until true; {Signature}
  :
  если создавался ключ на основании пароля - уничтожаем его,
  после чего освобождаем контекст криптопровайдера
  :
end;

Экспортированные таким образом открытые части ключей понадобятся нам для проверки подписи и расшифровки сеансового ключа.

Импорт ключевых пар во вновь созданный контейнер – это самостоятельная процедура. Необходимо запросить у пользователя название контейнера и пароль, подключиться к провайдеру, создать на основании пароля ключ, считать из файла импортируемые данные в буфер, после чего воспользоваться функцией CryptImportKey (провайдер, буфер, длина буфера, ключ для расшифровки, флаги, импортируемый ключ). Если нужно обеспечить возможность экспорта импортируемой ключевой пары впоследствии, то в параметре флаги необходимо передать значение CRYPT_EXPORTABLE; в противном случае вызов для данной ключевой пары функции CryptExportKey приведет к ошибке.

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

Электронная цифровая подпись

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

Подписать вычисленный хеш в CryptoAPI позволяет функция CryptSignHash (хеш, описание ключа, комментарий, флаги, подпись, длина подписи). Вторым параметром может быть либо AT_KEYEXCHANGE, либо AT_SIGNATURE (в нашем случае логичнее использовать ключ подписи). Третий параметр в целях безопасности настоятельно рекомендуется оставлять пустым (nil). Флаги в настоящее время также не используются – на месте этого аргумента должен быть нуль. Готовую электронную подпись функция запишет в буфер, адрес которого содержится в предпоследнем параметре, последний же параметр будет содержать длину подписи в байтах.

procedure TSigningForm.SignBtnClick(Sender: TObject);
var
  cont: PChar;
  err: string;
  hProv: HCRYPTPROV;
  key: HCRYPTKEY;
  alg: ALG_ID;
  hash: HCRYPTHASH;
  infile, outfile: file;
  size: DWORD;
  buf: array[0..511] of byte;
  signature: PBYTE;
begin{проверка существования выбранного файла}ifnot FileExists(DataNameEdit.Text) thenbegin
    MessageDlg('Неверное имя файла!', mtError, [mbOK], 0);
    exit;
  end;
  AssignFile(infile, DataNameEdit.Text);
  :
  "считываем" имя контейнера и подключаемся к нему
    :
  case HashRadioGroup.ItemIndex of
    0: alg := CALG_MD5;
    1: alg := CALG_SHA;
  end;
  CryptCreateHash(hProv, alg, 0, 0, @hash);
  SaveDialog1.Title := 'Задайте имя файла для хранения подписанных данных';
  if SaveDialog1.Execute thenbegin
    AssignFile(outfile, SaveDialog1.FileName);
    rewrite(outfile, 1);
    {записываем в файл идентификатор алгоритма хеширования}
    BlockWrite(outfile, alg, 4);
    reset(infile, 1);
    size := FileSize(infile);
    {записываем размер подписываемых данных}
    BlockWrite(outfile, size, 4);
    {пишем сами данные и вычисляем хеш:}whilenot eof(infile) dobegin
      BlockRead(infile, buf, 512, size);
      BlockWrite(outFile, buf, size);
      CryptHashData(hash, @buf, size, 0);
    end;
    CloseFile(infile);
    {выясняем размер подписи}
    CryptSignHash(hash, AT_SIGNATURE, nil, 0, nil, @size);
    {создаем подпись}
    GetMem(signature, size);
    CryptSignHash(hash, AT_SIGNATURE, nil, 0, signature, @size);
    BlockWrite(outfile, size, 4);
    BlockWrite(outfile, signature^, size);
    CloseFile(outfile);
  end;
  :
  уничтожаем хеш - объект и освобождаем контекст
  :
end;

Чтобы проверить правильность подписи, получатель подписанного сообщения должен иметь файл с открытым ключом подписи отправителя. В процессе проверки подписи этот ключ импортируется внутрь криптопровайдера. Проверка выполняется функцией CryptVerifySignature (хеш, подпись, длина подписи, открытый ключ, комментарий, флаги). О последних двух аргументах можно сказать то же, что и о параметрах комментарий и флаги функции CryptSignHash, назначение же остальных должно быть понятно. Если подпись верна, функция возвращает true. Значение false в качестве результата может свидетельствовать либо о возникновении ошибки в процессе проверки, либо о том, что подпись оказалась неверной. В последнем случае функция GetLastError вернет ошибку NTE_BAD_SIGNATURE. Для примера приведем наиболее значимые фрагменты программы проверки подписи:

Читайте также:  Усиленная неквалифицированная электронная подпись для всех пользователей «Личного кабинета для физических лиц» | ФНС России | 24 Красноярский край

procedure TMainForm.VerifyItemClick(Sender: TObject);
var
  err: string;
  hProv: HCRYPTPROV;
  key: HCRYPTKEY;
  alg: ALG_ID;
  hash: HCRYPTHASH;
  infile: file;
  size, test, textsize: DWORD;
  buf: PBYTE;
  signature, signkey: PBYTE;
begin
  :
  получаем контекст криптопровайдера
    :
  OpenDialog1.Title := 'Укажите файл с подписанными данными';
  if OpenDialog1.Execute thenbegin
    AssignFile(infile, OpenDialog1.FileName);
    reset(infile, 1);
    {считываем идентификатор алгоритма хеширования}
    BlockRead(infile, alg, 4);
    {считываем размер подписанных данных и сами данные}
    BlockRead(infile, textsize, 4);
    GetMem(buf, textsize);
    BlockRead(infile, buf^, textsize, test);
    if test < textsize thenbegin
      MessageDlg('Неверный формат файла! Процесс прерван.', mtError, [mbOK], 0);
      exit;
    end;
    {считываем размер подписи и саму подпись}
    BlockRead(infile, test, 4);
    GetMem(signature, test);
    BlockRead(infile, signature^, test);
    CloseFile(infile);
  endelse
    exit;
  :
  создаем хеш - объект и хешируем данные
    :
  OpenDialog1.Title := 'Укажите файл с открытым ключом подписи';
  if OpenDialog1.Execute thenbegin
    AssignFile(infile, OpenDialog1.FileName);
    reset(infile, 1);
    size := FileSize(infile);
    GetMem(signkey, size);
    BlockRead(infile, signkey^, size);
    CloseFile(infile);
  endelse
    exit;
  {импортируем открытый ключ подписи отправителя}
  CryptImportKey(hProv, signkey, size, 0, 0, @key);
  FreeMem(signkey, size);
  {проверяем подпись}if CryptVerifySignature(hash, signature, test, key, nil, 0) thenbegin
    MessageDlg('Подпись верна.', mtInformation, [mbOK], 0);
    {сохраняем подписанные данные}
    SaveDialog1.Title := 'Укажите имя файла для сохранения данных';
    if SaveDialog1.Execute thenbegin
      AssignFile(infile, SaveDialog1.FileName);
      rewrite(infile, 1);
      BlockWrite(infile, buf^, textsize);
      CloseFile(infile);
    end;
  endelsebegincase int64(GetLastError) of
      NTE_BAD_SIGNATURE: err := 'Подпись неверна!';
      {обработка других ошибок}else
      err := 'Ошибка при проверке подписи: Unknown error';
    end;
    MessageDlg(err, mtError, [mbOK], 0);
  end;
  :
  уничтожаем хеш - объект и импортированный ключ
  и освобождаем контекст криптопровайдера
  :
end;

От слов – к делу

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

Все эти операции подробно обсуждались в предыдущей статье. В простейшем случае “отправитель” сообщения может являться и “получателем” – тогда нужно просто экспортировать свой открытый ключ обмена ключами и сохранить его в каком-нибудь файле на диске.

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

Как реализовать методы криптографической защиты информации - Delphi Sources FAQ

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

procedure TMainForm.BitBtn1Click (Sender: TObject);
var
hProv: HCRYPTPROV;
KeyExchKey, SessionKey: HCRYPTKEY;
flag, keyLen: DWORD;
infile, outfile: file;
tmp: PBYTE;
buf: array [0..511] of byte;
alg: ALG_ID;
stream: boolean;
begin

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

if ActionRadioGroup.ItemIndex = 0 {шифрование}
then
begin
OpenDlg.Title:= ‘Укажите файл для шифрования’;
if OpenDlg.Execute then AssignFile (infile, OpenDlg.FileName)
else exit;
OpenDlg.Title:= ‘Укажите файл с открытым ключом обмена ключами получателя’;
if OpenDlg.Execute then
begin
AssignFile (outfile, OpenDlg.FileName);
reset (outfile, 1);
keyLen:= FileSize (outfile);
GetMem (tmp, keyLen);
BlockRead (outfile, tmp^, keyLen);
CloseFile (outfile);
end
else exit;
CryptImportKey (hProv, tmp, keyLen, 0, 0, @KeyExchKey);
FreeMem (tmp, keyLen);
SaveDlg.Title:= ‘Задайте имя файла для зашифрованных данных’;
if SaveDlg.Execute then AssignFile (outfile, SaveDlg.FileName)
else exit;
rewrite (outfile, 1);
case AlgRadioGroup.ItemIndex of {установка алгоритма шифрования}
0: begin
alg:= CALG_RC2; {алгоритм RC2}
stream:= false; {блочный шифр}
end;
1: begin
alg:= CALG_RC4; {алгоритм RC4}
stream:= true; {поточный шифр}
end;
end;
CryptGenKey (hProv, alg, CRYPT_EXPORTABLE or CRYPT_CREATE_SALT, @SessionKey); {создание сеансового ключа}
keyLen:= 128; {размер буфера “с запасом”}
GetMem (tmp, keyLen);
CryptExportKey (SessionKey, KeyExchKey, SIMPLEBLOB, 0, tmp, @keyLen);
BlockWrite (outfile, keyLen, 4); {запись в файл размера ключа}
BlockWrite (outfile, tmp^, keyLen); {и самого зашифрованного ключа}
CryptDestroyKey (KeyExchKey);
keyLen:= 512; {размер буфера “с запасом”}
CryptGetKeyParam (SessionKey, KP_SALT, @buf, @keyLen, 0);
BlockWrite (outfile, keyLen, 4); {запись в файл размера солта}
BlockWrite (outfile, buf, keyLen); {и самого солт-значения}
if not stream then {если шифр – блочный}
begin
//генерируем IV
keyLen:= 512; {размер буфера “с запасом”}
// запрос IV ради выяснения его размера
CryptGetKeyParam (SessionKey, KP_IV, @buf, @keyLen, 0);
CryptGenRandom (hProv, keyLen, @buf); {генерация IV}
CryptSetKeyParam (SessionKey, KP_IV, @buf, 0);
BlockWrite (outfile, keyLen, 4); {запись в файл размера IV}
BlockWrite (outfile, buf, keyLen); {и самого IV}
end;
reset (infile, 1);
while not eof (infile) do
begin {собственно шифрование и запись в файл}
BlockRead (infile, buf, 496, keyLen);
CryptEncrypt (SessionKey, 0, eof (infile), 0, @buf, @keyLen, 512);
BlockWrite (outfile, buf, keyLen);
end;
CloseFile (infile);
CloseFile (outfile);
CryptDestroyKey (SessionKey);
end
else {расшифровывание}
begin {получаем дескриптор своего ключа обмена ключами}
CryptGetUserKey (hProv, AT_KEYEXCHANGE, @KeyExchKey);
OpenDlg.Title:= ‘Укажите файл с зашифрованными данными’;
if OpenDlg.Execute then AssignFile (infile, OpenDlg.FileName)
else exit;
reset (infile, 1);
BlockRead (infile, keyLen, 4); {читаем размер ключа}
GetMem (tmp, keyLen);
BlockRead (infile, tmp^, keyLen); {читаем сам ключ}
CryptImportKey (hProv, tmp, keyLen, KeyExchKey, 0, @SessionKey);
FreeMem (tmp, keyLen);
CryptDestroyKey (KeyExchKey);
BlockRead (infile, keyLen, 4); {читаем солт-значение}
BlockRead (infile, buf, keyLen);
CryptSetKeyParam (SessionKey, KP_SALT, @buf, 0);
keyLen:= 4; {выясняем алгоритм шифрования}
CryptGetKeyParam (SessionKey, KP_ALGID, @alg, @keyLen, 0);
case alg of
CALG_RC2: stream:= false;
CALG_RC4: stream:= true;
end;
if not stream then {если шифр – блочный}
begin
//читаем и устанавливаем IV
BlockRead (infile, keyLen, 4);
BlockRead (infile, buf, keyLen);
CryptSetKeyParam (SessionKey, KP_IV, @buf, 0);
end;
SaveDlg.Title:= ‘Задайте имя файла для расшифрованных данных’;
if SaveDlg.Execute then
begin
AssignFile (outfile, SaveDlg.FileName);
rewrite (outfile, 1);
while not eof (infile) do
begin {собственно расшифровывание}
BlockRead (infile, buf, 512, keyLen);
CryptDecrypt (SessionKey, 0, eof (infile), 0, @buf, @keyLen);
BlockWrite (outfile, buf, keyLen);
end;
CloseFile (outfile);
end;
CloseFile (infile);
CryptDestroyKey (SessionKey);
end;
CryptReleaseContext (hProv, 0);
end;

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

Ссылки по теме

Оцените статью
ЭЦП Эксперт
Добавить комментарий