Delphi World - это проект, являющийся сборником статей и малодокументированных возможностей  по программированию в среде Delphi. Здесь вы найдёте работы по следующим категориям: delphi, delfi, borland, bds, дельфи, делфи, дэльфи, дэлфи, programming, example, программирование, исходные коды, code, исходники, source, sources, сорцы, сорсы, soft, programs, программы, and, how, delphiworld, базы данных, графика, игры, интернет, сети, компоненты, классы, мультимедиа, ос, железо, программа, интерфейс, рабочий стол, синтаксис, технологии, файловая система...
Методы криптографической защиты информации Windows

Оформил: DeeCo

Мы вступили в цифровой век. На смену бумажным документам пришли электронные, а личные контакты все чаще уступают место переписке по e-mail. Поэтому «шпионские штучки» вроде паролей и шифровок становятся все более привычными и необходимыми инструментами безопасности.

Криптографические возможности Windows

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

Итак, ОС мы доверяем. Чтобы криптозащиту нельзя было «обойти» с другой стороны — к примеру, перехватить из незащищенной области памяти секретные пароли — криптографические функции должны быть частью операционной системы. В семействе Windows, начиная с Windows 95, обеспечивается реализация шифрования, генерации ключей, создания и проверки цифровых подписей и других криптографических задач. Эти функции необходимы для работы операционной системы, однако ими может воспользоваться и любая прикладная программа — для этого программисту достаточно обратиться к нужной подпрограмме так, как предписывает криптографический интерфейс прикладных программ (CryptoAPI).

Разумеется, по мере совершенствования Windows расширялся и состав ее криптографической подсистемы. Помимо базовых операций, в настоящее время в CryptoAPI 2.0 поддерживается работа с сертификатами, шифрованными сообщениями в формате PKCS #7 и пр.

Описание функций CryptoAPI, помимо специальных книг, можно найти в MSDN Library, или в CD-версии, в файле crypto.chm.

Взаимодействие с 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), или криптопровайдеры

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

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

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

    По составу функций и обеспечивающих их алгоритмов криптопровайдеры подразделяются на типы. Например, любой 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 для получения версии провайдера следует учесть возможность возникновения ошибок следующим образом:

    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.

    Шифрование с использованием паролей

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

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

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

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

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

    При соблюдении приведенных условий хеш-значение служит компактным цифровым отпечатком (дайджестом) сообщения. Существует множество алгоритмов хеширования. CryptoAPI поддерживает, например, алгоритмы MD5 (MD — Message Digest) и SHA (Secure Hash Algorithm).

    Итак, чтобы создать ключ шифрования на основании пароля, нам нужно вначале получить хеш этого пароля. Для этого следует создать с помощью CryptoAPI хеш-объект, воспользовавшись функцией CryptCreateHash (провайдер, ID_алгоритма, ключ, флаги, хеш), которой нужно передать дескриптор криптопровайдера (полученный с помощью CryptAcquireContext) и идентификатор алгоритма хеширования (остальные параметры могут быть нулями). В результате мы получим дескриптор хеш-объекта. Этот объект можно представить себе как черный ящик, который принимает любые данные и «перемалывает» их, сохраняя внутри себя лишь хеш-значение. Подать данные на вход хеш-объекта позволяет функция CryptHashData (дескриптор, данные, размер_данных, флаги).

    Непосредственно создание ключа выполняет функция CryptDeriveKey (провайдер, ID_алгоритма, хеш-объект, флаги, ключ), которая принимает хеш-объект в качестве исходных данных и строит подходящий ключ для алгоритма шифрования, заданного своим ID. Результатом будет дескриптор ключа, который можно использовать для шифрования (рис. 3).

    Следует обратить внимание, что при работе с CryptoAPI мы все время имеем дело не с самими объектами или их адресами, а с дескрипторами — целыми числами, характеризующими положение объекта во внутренних таблицах криптопровайдера. Сами таблицы располагаются в защищенной области памяти, так что программы-«шпионы» не могут получить к ним доступ.

    Алгоритмы шифрования, поддерживаемые CryptoAPI, можно разделить на блочные и поточные: первые обрабатывают данные относительно большими по размеру блоками (например, 64, 128 битов или более), а вторые — побитно (теоретически, на практике же — побайтно). Если размер данных, подлежащих шифрованию, не кратен размеру блока, то последний, неполный блок данных, будет дополнен необходимым количеством случайных битов, в результате чего размер зашифрованной информации может несколько увеличиться. Разумеется, при использовании поточных шифров размер данных при шифровании остается неизменным.

    Шифрование выполняется функцией CryptEncrypt (ключ, хеш, финал, флаги, данные, рамер_данных, размер_буфера):

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

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

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

    {«описание» используемых переменных}
    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);

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

  • Проект Delphi World © Выпуск 2002 - 2024
    Автор проекта: USU Software
    Вы можете выкупить этот проект.