Блочные шифры
Блочные шифры считаются более надежными, нежели поточные, поскольку каждый блок текста подвергается сложным преобразованиям. Тем не менее, одних только этих преобразований оказывается недостаточно для обеспечения должного уровня безопасности - важно, каким образом они применяются к исходному тексту в процессе шифрования.
Инициализирующий вектор должен генерироваться отдельно с помощью уже известной нам функции CryptGenRandom и, как и солт-значение, передаваться вместе с ключом в открытом виде. Размер IV равен длине блока шифра. Например, для алгоритма RC2, поддерживаемого базовым криптопровайдером Microsoft, размер блока составляет 64 бита (8 байтов).
Целостность и аутентичность информации
Как удостовериться в том, что пришедшее сообщение действительно отправлено тем, чье имя стоит в графе "отправитель"? Асимметричные схемы шифрования дают нам элегантный способ аутентификации. Если отправитель зашифрует сообщение своим закрытым ключом, то успешное расшифровывание убедит получателя в том, что послать корреспонденцию мог только хозяин ключевой пары, и никто иной (рис. 6). При этом расшифровку может выполнить любой, кто имеет открытый ключ отправителя. Ведь наша цель - не конфиденциальность, а аутентификация.
Чтобы избежать шифрования всего сообщения при помощи асимметричных алгоритмов, используют хеширование: вычисляется хеш-значение исходного сообщения, и только эта короткая последовательность байтов шифруется закрытым ключом отправителя. Результат представляет собой электронную цифровую подпись. Добавление такой подписи к сообщению позволяет установить: аутентичность сообщения - создать подпись на основе закрытого ключа мог только его хозяин; целостность данных - легко вычислить хеш-значение полученного сообщения и сравнить его с тем, которое хранится в подписи: если значения совпадают, значит, сообщение не было изменено злоумышленником после того, как отправитель его подписал.
Таким образом, асимметричные алгоритмы позволяют решить две непростые задачи: обмена ключами шифрования по открытым каналам связи и подписи сообщения. Чтобы воспользоваться этими возможностями, нужно сгенерировать и сохранить две ключевые пары - для обмена ключами и для подписей. В этом нам поможет CryptoAPI.
в цифровой век. На смену
Мы вступили в цифровой век. На смену бумажным документам пришли электронные, а личные контакты все чаще уступают место переписке по e-mail. Поэтому «шпионские штучки» вроде паролей и шифровок становятся все более привычными и необходимыми инструментами безопасности.
В арсенале защиты должны быть
В арсенале защиты должны быть не только методы, обеспечивающие секретность передачи информации (о них мы говорили в первой части статьи). Не менее важными инструментами безопасности являются процедуры, позволяющие убедиться в целости и аутентичности данных. Кроме того, необходимо решать проблемы безопасного хранения и распределения ключей.
в любой точке земного шара
Для конфиденциального обмена информацией с корреспондентом в любой точке земного шара приходится использовать целый арсенал современных криптографических инструментов: симметричные и асимметричные алгоритмы шифрования, механизмы генерирования криптографических ключей и случайных последовательностей, специфические режимы работы шифров ипр. Продолжая тему, начатую в первой и второй частях статьи, рассмотрим реализацию этих инструментов в CryptoAPI и воспользуемся ими для шифрования файла случайным ключом.
Цифровые конверты
Delphi и Windows API для защиты секретов
Константин Виноградов, Лилия Виноградова,
Как реализовать методы криптографической защиты информации при помощи подручных средств – Windows и Delphi
Электронная цифровая подпись
Подписать вычисленный хеш в CryptoAPI позволяет функция CryptSignHash (хеш, описание ключа, комментарий, флаги, подпись, длина подписи). Вторым параметром может быть либо AT_KEYEXCHANGE, либо AT_SIGNATURE (в нашем случае логичнее использовать ключ подписи). Третий параметр в целях безопасности настоятельно рекомендуется оставлять пустым (nil). Флаги в настоящее время также не используются - на месте этого аргумента должен быть нуль. Готовую электронную подпись функция запишет в буфер, адрес которого содержится в предпоследнем параметре, последний же параметр будет содержать длину подписи в байтах.
Чтобы проверить правильность подписи, получатель подписанного сообщения должен иметь файл с открытым ключом подписи отправителя. В процессе проверки подписи этот ключ импортируется внутрь криптопровайдера. Проверка выполняется функцией CryptVerifySignature (хеш, подпись, длина подписи, открытый ключ, комментарий, флаги). О последних двух аргументах можно сказать то же, что и о параметрах комментарий и флаги функции CryptSignHash, назначение же остальных должно быть понятно. Если подпись верна, функция возвращает true. Значение false в качестве результата может свидетельствовать либо о возникновении ошибки в процессе проверки, либо о том, что подпись оказалась неверной. В последнем случае функция GetLastError вернет ошибку NTE_BAD_SIGNATURE. Для примера приведем наиболее значимые фрагменты программы проверки подписи: См. Листинг
Полный Delphi-проект примера можно найти здесь.
Контейнеры ключей
Подключение к контейнеру производится одновременно с получением контекста криптопровайдера при вызове функции CryptAcquireContext - имя контейнера ключей передается функции вторым ее аргументом. Если второй аргумент содержит пустой указатель (nil), то используется имя по умолчанию, т. е. имя пользователя. В том случае, если доступ к контейнеру не нужен, можно передать в последнем аргументе функции флаг CRYPT_VERIFYCONTEXT; при необходимости создать новый контейнер используется флаг CRYPT_NEWKEYSET; а для удаления существующего контейнера вместе с хранящимися в нем ключами - CRYPT_DELETEKEYSET.
Каждый контейнер может содержать, как минимум, две ключевые пары - ключ обмена ключами и ключ подписи. Ключи, используемые для шифрования симметричными алгоритмами, не сохраняются. Как мы уже говорили, такие ключи не рекомендуется применять более одного раза, поэтому их называют сеансовыми (англ. session key).
Криптографические возможности Windows
Сразу договоримся, что никакая система защиты информации не может быть абсолютно надежной. Речь может идти лишь о некоторой степени надежности и рисках, связанных со взломом защиты. Поэтому с практической точки зрения есть смысл оценить важность данных и экономно подстелить соломку на случай неудачи. В наших приложениях, например, мы выдаем кредит доверия операционной системе Windows, несмотря на закрытость ее кода.
Итак, ОС мы доверяем. Чтобы криптозащиту нельзя было «обойти» с другой стороны - к примеру, перехватить из незащищенной области памяти секретные пароли - криптографические функции должны быть частью операционной системы. В семействе Windows, начиная с Windows 95, обеспечивается реализация шифрования, генерации ключей, создания и проверки цифровых подписей и других криптографических задач. Эти функции необходимы для работы операционной системы, однако ими может воспользоваться и любая прикладная программа - для этого программисту достаточно обратиться к нужной подпрограмме так, как предписывает криптографический интерфейс прикладных программ (CryptoAPI).
Разумеется, по мере совершенствования Windows расширялся и состав ее криптографической подсистемы. Помимо базовых операций, в настоящее время в CryptoAPI 2.0 поддерживается работа с сертификатами, шифрованными сообщениями в формате PKCS #7 и пр.
Описание функций CryptoAPI, помимо специальных книг, можно найти в MSDN Library, или в CD-версии, в файле crypto.chm.
then begin case
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;
record algID: ALG_ID; dwBits: DWORD;
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;
используемых переменных} hProv: HCRYPTPROV; hash:
{«описание» используемых переменных} hProv: HCRYPTPROV; hash: HCRYPTHASH; password: string; key: HCRYPTKEY; plaintext, ciphertext: string; inFile, outFile: file; data: PByte; l: DWORD;
{получаем контекст криптопровайдера} CryptAcquireContext(@hProv, nil, nil, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT); {создаем хеш-объект} CryptCreateHash(hProv, CALG_SHA, 0, 0, @hash); {хешируем пароль} CryptHashData(hash, @password[1], length(password), 0); {создаем ключ на основании пароля для потокового шифра RC4} CryptDeriveKey(hProv, CALG_RC4, hash, 0, @key); {уничтожаем хеш-объект} CryptDestroyHash(hash); {открываем файлы} AssignFile(inFile, plaintext); AssignFile(outFile, ciphertext); reset(inFile, 1); rewrite(outFile, 1); {выделяем место для буфера} GetMem(data, 512); {шифруем данные} while not eof(inFile) do begin BlockRead(inFile, data^, 512, l); CryptEncrypt(key, 0, eof(inFile), 0, data, @l, l); BlockWrite(outFile, data^, l); end; {освобождаем место и закрываем файлы} FreeMem(data, 512); CloseFile(inFile); CloseFile(outFile); {освобождаем контекст криптопровайдера} CryptReleaseContext(hProv, 0);
var cont: PChar; err: string;
procedure TGenerateForm.OKBtnClick(Sender: TObject); var cont: PChar; err: string; hProv: HCRYPTPROV; KeyExchKey, SignKey: HCRYPTKEY; flag, keyLen: DWORD; begin {если ни один ключ не выбран - выход} if not (KEKCheckBox.Checked or SKCheckBox.Checked) then exit; {"считываем" имя контейнера} if length(ContainerEdit.Text) = 0 then cont := nil else begin 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 then begin {"считываем" длину ключа и помещаем ее в старшее слово параметра ФЛАГИ} keyLen := strtoint(KeyExchLenEdit.text); flag := keyLen shl 16; if not CryptGenKey(hProv, AT_KEYEXCHANGE, flag, @KeyExchKey) then begin јобработка ошибокј end else begin ReportMemo.Lines.Add(''); ReportMemo.Lines.Add('Создан ключ обмена ключами:'); flag := 4; if not CryptGetKeyParam(KeyExchKey, KP_KEYLEN, @keyLen, @flag, 0) then begin јобработка ошибокј end else ReportMemo.Lines.Add(' длина ключа - ' + inttostr(keyLen)); flag := 4; if not CryptGetKeyParam(KeyExchKey, KP_ALGID, @keyLen, @flag, 0) then begin јобработка ошибокј end else ReportMemo.Lines.Add(' алгоритм - ' + algIDtostr(keyLen)); {функция algIDtostr здесь не приводится. Она состоит из единственного оператора case, отображающего целый идентификатор алгоритма в строку} end; end; {генерация ключа подписи (Signature Key)} if SKCheckBox.Checked then begin јвыполняется аналогично генерации ключа обмена ключамиј end; CryptReleaseContext(hProv, 0); end;
var cont: PChar; err: string;
procedure TExportForm.OKBtnClick(Sender: TObject); var cont: PChar; err: string; hProv: HCRYPTPROV; key, expKey: HCRYPTKEY; pbuf: PBYTE; buflen: DWORD; f: file; hash: HCRYPTHASH; begin {если ни один ключ не выбран - выход} if not (KEKCheckBox.Checked or SKCheckBox.Checked) then exit; {если нужен пароль, т.е. экспортируется ключевая пара целиком} if PasswEdit.Enabled and (PasswEdit.Text <> Passw2Edit.Text) then begin MessageDlg('Ошибка при вводе пароля! Повторите ввод.', mtError, [mbOK], 0); exit; end; … "считываем" имя контейнера и подключаемся к криптопровайдеру … если нужен ключ шифрования - создаем его на основании пароля … {ключ обмена ключами} if KEKCheckBox.Checked then repeat {получаем дескриптор ключа} 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 then begin AssignFile(f, SaveDialog1.FileName); rewrite(f, 1); BlockWrite(f, pbuf^, bufLen); CloseFile(f); MessageDlg('Ключ обмена ключами успешно сохранен', mtInformation, [mbOK], 0); end; until true; {KeyExchange} {ключ подписи} if SKCheckBox.Checked then repeat јаналогично ключу обмена ключамиј until true; {Signature} … если создавался ключ на основании пароля - уничтожаем его, после чего освобождаем контекст криптопровайдера … end;
var cont: PChar; err: string;
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 {проверка существования выбранного файла} if not FileExists(DataNameEdit.Text) then begin 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 then begin AssignFile(outfile, SaveDialog1.FileName); rewrite(outfile, 1); {записываем в файл идентификатор алгоритма хеширования} BlockWrite(outfile, alg, 4); reset(infile, 1); size := FileSize(infile); {записываем размер подписываемых данных} BlockWrite(outfile, size, 4); {пишем сами данные и вычисляем хеш:} while not eof(infile) do begin 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;
var err: string; hProv: HCRYPTPROV;
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 then begin 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 then begin MessageDlg('Неверный формат файла! Процесс прерван.', mtError, [mbOK], 0); exit; end; {считываем размер подписи и саму подпись} BlockRead(infile, test, 4); GetMem(signature, test); BlockRead(infile, signature^, test); CloseFile(infile); end else exit; … создаем хеш-объект и хешируем данные … OpenDialog1.Title := 'Укажите файл с открытым ключом подписи'; if OpenDialog1.Execute then begin AssignFile(infile, OpenDialog1.FileName); reset(infile, 1); size := FileSize(infile); GetMem(signkey, size); BlockRead(infile, signkey^, size); CloseFile(infile); end else exit; {импортируем открытый ключ подписи отправителя} CryptImportKey(hProv, signkey, size, 0, 0, @key); FreeMem(signkey, size); {проверяем подпись} if CryptVerifySignature(hash, signature, test, key, nil, 0) then begin MessageDlg('Подпись верна.', mtInformation, [mbOK], 0); {сохраняем подписанные данные} SaveDialog1.Title := 'Укажите имя файла для сохранения данных'; if SaveDialog1.Execute then begin AssignFile(infile, SaveDialog1.FileName); rewrite(infile, 1); BlockWrite(infile, buf^, textsize); CloseFile(infile); end; end else begin case int64(GetLastError) of NTE_BAD_SIGNATURE: err := 'Подпись неверна!'; {обработка других ошибок} else err := 'Ошибка при проверке подписи: Unknown error'; end; MessageDlg(err, mtError, [mbOK], 0); end; … уничтожаем хеш-объект и импортированный ключ и освобождаем контекст криптопровайдера … end;
var hProv: HCRYPTPROV; KeyExchKey, SessionKey:
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;
Обмен ключами
Теперь мы располагаем набором ключей, однако все они останутся мертвым грузом, до тех пор пока мы не получим возможности обмена с другими пользователями открытыми ключами. Для этого необходимо извлечь их из базы данных ключей и записать в файл, который можно будет передать своим корреспондентам. При экспорте данные ключа сохраняются в одном из трех возможных форматов: PUBLICKEYBLOB - используется для сохранения открытых ключей. Поскольку открытые ключи не являются секретными, они сохраняются в незашифрованном виде; PRIVATEKEYBLOB - используется для сохранения ключевой пары целиком (открытого и закрытого ключей). Эти данные являются в высшей степени секретными, поэтому сохраняются в зашифрованном виде, причем для шифрования используется сеансовый ключ (и, соответственно, симметричный алгоритм); SIMPLEBLOB - используется для сохранения сеансовых ключей. Для обеспечения секретности данные ключа шифруются с использованием открытого ключа получателя сообщения.
Экспорт ключей в CryptoAPI выполняется функцией CryptExportKey (экспортируемый ключ, ключ адресата, формат, флаги, буфер, размер буфера): экспортируемый ключ - дескриптор нужного ключа; ключ адресата - в случае сохранения открытого ключа должен быть равен нулю (данные не шифруются); формат - указывается один из возможных форматов экспорта (PUBLICKEYBLOB, PRIVATEKEYBLOB, SIMPLEBLOB); флаги - зарезервирован на будущее (должен быть равен нулю); буфер - содержит адрес буфера, в который будет записан ключевой BLOB (Binary Large OBject - большой двоичный объект); размер буфера - при вызове функции в этой переменной должен находиться доступный размер буфера, а по окончании работы в нее записывается количество экспортируемых данных. Если размер буфера заранее не известен, то функцию нужно вызвать с параметром буфер, равным пустому указателю, тогда размер буфера будет вычислен и занесен в переменную размер буфера.
Запросить у криптопровайдера дескриптор самого' экспортируемого ключа позволяет функция CryptGetUserKey (провайдер, описание ключа, дескриптор ключа). Описание ключа - это либо AT_KEYEXCHANGE, либо AT_SIGNATURE.
Экспорт асимметричных ключей во всем возможном многообразии можно осуществить при помощи формы, показанной на рис. 9.
В Листинге приведены наиболее важные фрагменты программы
Экспортированные таким образом открытые части ключей понадобятся нам для проверки подписи и расшифровки сеансового ключа.
Импорт ключевых пар во вновь созданный контейнер - это самостоятельная процедура. Необходимо запросить у пользователя название контейнера и пароль, подключиться к провайдеру, создать на основании пароля ключ, считать из файла импортируемые данные в буфер, после чего воспользоваться функцией CryptImportKey (провайдер, буфер, длина буфера, ключ для расшифровки, флаги, импортируемый ключ). Если нужно обеспечить возможность экспорта импортируемой ключевой пары впоследствии, то в параметре флаги необходимо передать значение CRYPT_EXPORTABLE; в противном случае вызов для данной ключевой пары функции CryptExportKey приведет к ошибке.
От слов - к делу
Приведем основные фрагменты процедуры, осуществляющей шифрование и расшифровку файла (обработка ошибок опущена): См. пример
В рассмотренной нами процедуре обмена шифрованными сообщениями остается одно слабое звено - обмен открытыми ключами. Ведь при этом мы не обеспечиваем подлинность полученного ключа - во время пересылки его может подменить злоумышленник. CryptoAPI для решения этой проблемы предполагает использование сертификатов. Но об этом - в следующий раз.
Полный Delphi-проект можно взять здесь.
Проблема распределения ключей
В прошлый раз при помощи CryptoAPI мы решали такую "классическую" задачу как шифрование на основе пароля. Напомним, что пароль использовался для создания ключа шифрования какого-либо симметричного алгоритма. В таком случае расшифровать файл может лишь тот, кто знает пароль. А значит, для обеспечения конфиденциальности нужно держать пароль в строжайшем секрете - желательно, чтобы его знали лишь отправитель и получатель информации. (А еще лучше, если отправитель и получатель - одно и то же лицо.)
Предположим, что отправитель и получатель при личной встрече договорились использовать для конфиденциальной переписки определенный пароль. Но если они будут шифровать все свои сообщения одним и тем же ключом, то возможный противник, перехватив корреспонденцию, будеть иметь хорошие шансы взломать шифр: при современных методах криптоанализа наличие нескольких шифртекстов, полученных путем использования одного и того же ключа, почти гарантирует успешный результат. Поэтому при использовании симметричных алгоритмов шифрования настоятельно рекомендуется не применять один и тот же ключ дважды!
Однако помнить отдельный пароль для каждого зашифрованного сообщения - задача достаточно трудоемкая. А для корреспондентов, не имеющих возможности встретиться лично для согласования ключей шифрования, конфиденциальный обмен сообщениями вообще становится недоступным. Такая практическая трудность называется проблемой распределения ключей.
Описанная схема реализована и в CryptoAPI.
Шифрование с использованием паролей
После того как мы узнали кое-что о структуре CryptoAPI, можно воспользоваться ею в практических целях. Пожалуй, самым ожидаемым действием криптографической подсистемы является шифрование файлов - так, чтобы лишь пользователь, знающий определенный пароль, мог получить к ним доступ.
Для шифрования данных в CryptoAPI применяются симметричные алгоритмы. Симметричность означает, что для шифрования и расшифровки данных используется один и тот же ключ, известный как шифрующей, так и расшифровывающей стороне. При этом плохо выбранный ключ шифрования может дать противнику возможность взломать шифр. Поэтому одной из функций криптографической подсистемы должна быть генерация «хороших» ключей либо случайным образом, либо на основании некоторой информации, предоставляемой пользователем, например пароля.
В случае создания ключа на основании пароля должно выполняться следующее обязательное условие: при многократном повторении процедуры генерации ключа на одном и том же пароле должны получаться идентичные ключи. Ключ шифрования имеет, как правило, строго определенную длину, определяемую используемым алгоритмом, а длина пароля может быть произвольной. Даже интуитивно понятно, что для однозначной генерации ключей нужно привести разнообразные пароли к некоторой единой форме. Это достигается с помощью хеширования.
Хешированием (от англ. hash - разрезать, крошить, перемешивать) называется преобразование строки произвольной длины в битовую последовательность фиксированной длины (хеш-значение, или просто хеш) с обеспечением следующих условий: по хеш-значению невозможно восстановить исходное сообщение; практически невозможно найти еще один текст, дающий такой же хеш, как и наперед заданное сообщение; практически невозможно найти два различных текста, дающих одинаковые хеш-значения (такие ситуации называют коллизиями).
При соблюдении приведенных условий хеш-значение служит компактным цифровым отпечатком (дайджестом) сообщения. Существует множество алгоритмов хеширования. CryptoAPI поддерживает, например, алгоритмы MD5 (MD - Message Digest) и SHA (Secure Hash Algorithm).
Итак, чтобы создать ключ шифрования на основании пароля, нам нужно вначале получить хеш этого пароля. Для этого следует создать с помощью CryptoAPI хеш-объект, воспользовавшись функцией CryptCreateHash (провайдер, ID_алгоритма, ключ, флаги, хеш), которой нужно передать дескриптор криптопровайдера (полученный с помощью CryptAcquireContext) и идентификатор алгоритма хеширования (остальные параметры могут быть нулями). В результате мы получим дескриптор хеш-объекта. Этот объект можно представить себе как черный ящик, который принимает любые данные и «перемалывает» их, сохраняя внутри себя лишь хеш-значение. Подать данные на вход хеш-объекта позволяет функция CryptHashData (дескриптор, данные, размер_данных, флаги).
Непосредственно создание ключа выполняет функция CryptDeriveKey (провайдер, ID_алгоритма, хеш-объект, флаги, ключ), которая принимает хеш-объект в качестве исходных данных и строит подходящий ключ для алгоритма шифрования, заданного своим ID. Результатом будет дескриптор ключа, который можно использовать для шифрования (рис. 3).
Алгоритмы шифрования, поддерживаемые CryptoAPI, можно разделить на блочные и поточные: первые обрабатывают данные относительно большими по размеру блоками (например, 64, 128 битов или более), а вторые - побитно (теоретически, на практике же - побайтно). Если размер данных, подлежащих шифрованию, не кратен размеру блока, то последний, неполный блок данных, будет дополнен необходимым количеством случайных битов, в результате чего размер зашифрованной информации может несколько увеличиться. Разумеется, при использовании поточных шифров размер данных при шифровании остается неизменным.
Шифрование выполняется функцией CryptEncrypt (ключ, хеш, финал, флаги, данные, рамер_данных, размер_буфера): через параметр ключ передается дескриптор ключа шифрования; параметр хеш используется, если одновременно с шифрованием нужно вычислить хеш-значение шифруемого текста; параметр финал равен true, если шифруемый блок текста - последний или единственный (шифрование можно осуществлять частями, вызывая функцию CryptEncrypt несколько раз); значение флага должно быть нулевым; параметр данные представляет собой адрес буфера, в котором при вызове функции находится исходный текст, а по завершению работы функции - зашифрованный; следующий параметр, соответственно, описывает размер входных/выходных данных, последний параметр задает размер буфера - если в результате шифрования зашифрованный текст не уместится в буфере, возникнет ошибка.
Для расшифровки данных используется функция CryptDecrypt (ключ, хеш, финал, флаги, данные, рамер_данных), отличающаяся от шифрующей функции только тем, что размер буфера указывать не следует: поскольку размер данных при расшифровке может только уменьшиться, отведенного под них буфера наверняка будет достаточно.
Приведем лишь фрагменты программы, реализующей шифрование файла с использованием заданного пароля, опустив громоздкие проверки успешности выполнения криптографических операций (что в реальной программе делать крайне нежелательно).
Полный пример приложения в формате Delphi 4 можно взять здесь.
Конечно, шифрование вами всех файлов одним и тем же паролем облегчает «противнику» задачу их расшифровки, запоминание огромного числа паролей сильно усложняет жизнь, а их записывание в незашифрованном виде создает опасность раскрытия всей системы. CryptoAPI может предложить на этот случай ряд решений. О них поговорим ниже.
Создание ключевых пар
После создания контейнера ключей необходимо сгенерировать ключевые пары обмена ключами и подписи. Эту работу в CryptoAPI выполняет функция CryptGenKey (провайдер, алгоритм, флаги, ключ): провайдер - дескриптор криптопровайдера, полученный в результате обращения к функции CryptAcquireContext; алгоритм - указывает, какому алгоритму шифрования будет соответствовать создаваемый ключ. Информация об алгоритме, таким образом, является частью описания ключа. Каждый криптопровайдер использует для обмена ключами и подписи строго определенные алгоритмы. Так, провайдеры типа PROV_RSA_FULL, к которым относится и Microsoft Base Cryptographic Provider, реализуют алгоритм RSA. Но при генерации ключей знать это не обязательно: достаточно указать, какой ключ мы собираемся создать - обмена ключами или подписи. Для этого используются мнемонические константы AT_KEYEXCHANGE и AT_SIGNATURE; флаги - при создании асимметричных ключей управляет их размером. Используемый нами криптопровайдер позволяет генерировать ключ обмена ключами длиной от 384 до 512 бит**, а ключ подписи - от 512 до 16384 бит. Чем больше длина ключа, тем выше его надежность, поэтому трудно найти причины для использования ключа обмена ключами длиной менее 512 бит, а длину ключа подписи не рекомендуется делать меньше 1024 бит**. По умолчанию криптопровайдер создает оба ключа длиной 512 бит. Необходимую длину ключа можно передать в старшем слове параметра флаги; ключ - в случае успешного завершения функции в этот параметр заносится дескриптор созданного ключа.
Создание сеансовых ключей
CryptoAPI позволяет генерировать сеансовые ключи случайным образом - эту работу выполняет функция CryptGenKey, о которой шла речь ранее. Однако при использовании этой возможности за пределами США и Канады приходится учитывать американские ограничения на экспорт средств "сильной криптографии". В частности, до января 2000года был запрещен экспорт программного обеспечения для шифрования с использованием ключей длиной более 40 бит. Этим объясняется разработка Microsoft двух версий своего криптопровайдера - базовой и расширенной. Базовая версия предназначалась на экспорт и поддерживала симметричные ключи длиной 40 бит; расширенная же версия (Microsoft Enhanced Cryptographic Provider) работала с "полной" длиной ключа (128 бит). Поскольку алгоритм шифрования, как правило, требует использования ключа строго определенной длины, недостающее количество битв в урезанном "экспортном" ключе могло быть заполнено либо нулями, либо случайными данными, которые предлагалось передавать открыто.
В криптографической практике внесение в состав ключа определенной части несекретных данных, которые сменяются несколько раз в ходе обработки исходного или шифр-текста, используется для того, чтобы воспрепятствовать взлому шифра атакой "по словарю". В английской терминологии такие вставки называются salt values: их назначение - "подсолить" ключ (с учетом нашей ментальности можно перевести как "насолить" противнику). Поскольку этот термин используется и в CryptoAPI, будем употреблять его в транслитерированном виде - солт-значения.
Итак, CryptoAPI, в экспортном исполнении практически вынуждает нас использовать солт-значения, составляющие бОльшую часть ключа - 88 бит из 128-ми для симметричных алгоритмов в RC2; и RC4. Конечно, при такой эффективной длине ключа криптозащита не может считаться достаточно надежной. В реальной ситуации выход один - воспользоваться криптопровайдером, не ограничивающим длину ключа. Обладатели Windows XP могут прибегнуть к услугам расширенных версий провайдера Microsoft (Enhanced или Strong). Пользователям более старых версий Windows, по-видимому, придется воспользоваться продуктами сторонних разработчиков. Например, свои версии криптопровайдеров предлагают российская компания "Крипто-Про" и шведская . В Украине, насколько известно авторам, разработкой национального провайдера криптографических услуг занимается коллектив харьковских ученых под руководством профессора Горбенко, однако до широкого внедрения дело пока не дошло. Тем не менее, благодаря архитектуре CryptoAPI, прикладные программы могут разрабатываться и отлаживаться и с базовым провайдером Microsoft - так как интерфейс взаимодействия остается неизменным. Поэтому вернемся к обсуждению создания случайных сеансовых ключей.
Солт может быть сгенерирован вместе с ключом: для этого нужно в качестве флага передать функции CryptGenKey (или CryptDeriveKey) константу CRYPT_CREATE_SALT. Правда, при сохранении ключа (с помощью функции CryptExportKey) система уже не заботится о солт-значении, перекладывая ответственность на прикладную программу. Таким образом, корректная процедура создания и сохранения симметричного ключа предполагает:
1. при создании ключа функции CryptGenKey передается значение флага CRYPT_EXPORTABLE or CRYPT_CREATE_SALT;
2. с помощью функции CryptGetKeyParam с параметром KP_SALT сгенерированное солт-значение сохраняется в буфере;
3. ключ в зашифрованном виде сохраняется в буфере при помощи функции CryptExportKey, которой передается открытый ключ обмена ключами адресата;
4. зашифрованные ключевые данные сохраняются или передаются адресату вместе с экспортированным на втором шаге солт-значением.
С другой стороны, солт-значение может быть сгенерировано и отдельно от ключа. Для этого используется функция CryptGenRandom (провайдер, длина, буфер). Здесь параметр длина задает размер генерируемой случайной последовательности в байтах, а последний аргумент задает адрес буфера, в который будет записан результат. Полученное таким образом солт-значение может быть внесено в ключ с помощью функции CryptSetKeyParam (ключ, параметр, данные, флаги). Ей вторым аргументом нужно передать KP_SALT, а третьим - адрес буфера, содержащего сгенерированную последовательность. (Последний аргумент функции зарезервирован на будущее и должен быть равен нулю.)
Взаимодействие с CryptoAPI
Функции CryptoAPI можно вызвать из программы, написанной на любимом многими (в том числе и авторами) языке С++. Тем не менее, Pascal де-факто признан стандартом в области обучения программированию. (Не будем спорить о том, хорошо это или плохо, чтобы не ввязываться в драку, пусть даже и виртуальную.) Кроме того, в ряде отечественных компаний Delphi является базовым средством разработки. Поэтому все примеры были реализованы в среде Delphi. Хотя в качестве инструмента можно было бы выбрать и MS Visual C++.
Код функций криптографической подсистемы содержится в нескольких динамически загружаемых библиотеках Windows (advapi32.dll, crypt32.dll). Для обращения к такой функции из прикладной программы на Object Pascal следует объявить ее как внешнюю. Заголовок функции в интерфейсной части модуля будет выглядеть, например, так: function CryptAcquireContext ( phPROV: PHCRYPTPROV; pszContainer: LPCTSTR; pszProvider: LPCTSTR; dwProvType: DWORD; dwFlags: DWORD): BOOL; stdcall;
а в исполняемой части вместо тела функции нужно вписать директиву extern с указанием библиотеки, в которой содержится функция, и, возможно, ее имени в этой библиотеке (если оно отличается от имени функции в создаваемом модуле), например: function CryptAcquireContext; external ‘advapi32.dll’ name 'CryptAcquireContextA';
Таким образом, имея описание функций CryptoAPI, можно собрать заголовки функций в отдельном модуле, который будет обеспечивать взаимодействие прикладной программы с криптографической подсистемой. Разумеется, такая работа была проделана программистами Microsoft, и соответствующий заголовочный файл (wincrypt.h) был включен в поставку MS Visual C++. К счастью, появилась и Delphi-версия (wcrypt2.pas). Ее можно найти здесь. Подключив модуль к проекту, вы сможете использовать не только функции CryptoAPI, но и мнемонические константы режимов, идентификаторы алгоритмов и прочих параметров, необходимых на практике.
И последнее замечание перед тем, как опробовать CryptoAPI в деле. Ряд функций был реализован только в Windows 2000. Но и на старушку Windows 98 можно найти управу: при установке Internet Explorer 5 интересующие нас библиотеки обновляются, позволяя использовать новейшие криптографические возможности. Нужно лишь задать для Delphi-проекта параметр условной компиляции NT5, после чего вызовы функций, появившихся лишь в Windows 2000, будут нормально работать.
Знакомство с криптопровайдерами
Функции CryptoAPI обеспечивают прикладным программам доступ к криптографическим возможностям Windows. Однако они являются лишь «передаточным звеном» в сложной цепи обработки информации. Основную работу выполняют скрытые от глаз программиста функции, входящие в специализированные программные (или программно-аппаратные) модули — провайдеры (поставщики) криптографических услуг (CSP - Cryptographic Service Providers), или криптопровайдеры (рис. 1).
Криптопровайдеры отличаются друг от друга:
составом функций (например, некоторые криптопровайдеры не выполняют шифрование данных, ограничиваясь созданием и проверкой цифровых подписей); требованиями к оборудованию (специализированные криптопровайдеры могут требовать устройства для работы со смарт-картами для выполнения аутентификации пользователя); алгоритмами, осуществляющими базовые действия (создание ключей, хеширование и пр.).
По составу функций и обеспечивающих их алгоритмов криптопровайдеры подразделяются на типы. Например, любой CSP типа PROV_RSA_FULL поддерживает как шифрование, так и цифровые подписи, использует для обмена ключами и создания подписей алгоритм RSA, для шифрования — алгоритмы RC2 и RC4, а для хеширования - MD5 и SHA.
В зависимости от версии операционной системы состав установленных криптопровайдеров может существенно изменяться. Однако на любом компьютере с Windows можно найти Microsoft Base Cryptographic Provider, относящийся к уже известному нам типу PROV_RSA_FULL. Именно с этим провайдером по умолчанию будут взаимодействовать все программы.
Использование криптографических возможностей Windows напоминает работу программы с графическим устройством. Криптопровайдер подобен графическому драйверу: он может обеспечивать взаимодействие программного обеспечения с оборудованием (устройство чтения смарт-карт, аппаратные датчики случайных чисел и пр.). Для вывода информации на графическое устройство приложение не должно непосредственно обращаться к драйверу — вместо этого нужно получить у системы контекст устройства, посредством которого и осуществляются все операции. Это позволяет прикладному программисту использовать графическое устройство, ничего не зная о его аппаратной реализации. Точно так же для использования криптографических функций приложение обращается к криптопровайдеру не напрямую, а через CryptoAPI. При этом вначале необходимо запросить у системы контекст криптопровайдера.
Первым делом, хотя бы из любопытства, выясним, какие же криптопровайдеры установлены в системе. Для этого нам понадобятся четыре функции CryptoAPI (выходные параметры выделены жирным шрифтом, а входные - курсивом): CryptEnumProviders (i, резерв, флаги, тип, имя, длина_имени) - возвращает имя и тип i-го по порядку криптопровайдера в системе (нумерация начинается с нуля); CryptAcquireContext (провайдер, контейнер, имя, тип, флаги) - выполняет подключение к криптопровайдеру с заданным типом и именем и возвращает его дескриптор (контекст). При подключении мы будем передавать функции флаг CRYPT_VERIFYCONTEXT, служащий для получения контекста без подключения к контейнеру ключей; CryptGetProvParam (провайдер, параметр, данные, размер_данных, флаги) - возвращает значение указанного параметра провайдера, например, версии (второй параметр при вызове функции - PP_VERSION), типа реализации (программный, аппаратный, смешанный - PP_IMPTYPE), поддерживаемых алгоритмов (PP_ENUMALGS). Список поддерживаемых алгоритмов при помощи этой функции может быть получен следующим образом: при одном вызове функции возвращается информация об одном алгоритме; при первом вызове функции следует передать значение флага CRYPT_FIRST, а при последующих флаг должен быть равен 0; CryptReleaseContext (провайдер, флаги) - освобождает дескриптор криптопровайдера.
Каждая из этих функций, как и большинство других функций CryptoAPI, возвращает логическое значение, равное true, в случае успешного завершения, и false - если возникли ошибки. Код ошибки может быть получен при помощи функции GetLastError. Возможные значения кодов ошибки приведены в упоминавшейся выше документации. Например, при вызове функции CryptGetProvParam для получения версии провайдера следует учесть возможность возникновения ошибок следующим образом: см. Лист 1
Текст процедуры, выводящей в Memo-поле FileMemo формы информацию об установленных в системе криптопровайдерах, приведен в Лист 2. Предполагается, что процедура вызывается при выборе соответствующего элемента в главном меню формы. Для краткости в тексте программы опущены фрагменты, выполняющие обработку ошибок.