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

Оформил: DeeCo

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

Цифровые конверты

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

Создание сеансовых ключей

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

Блочные шифры

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

Наиболее простой и интуитивно понятный способ состоит в том, чтобы разбить исходный текст на блоки соответствующего размера, а затем отдельно каждый блок подвергнуть шифрующему преобразованию. Такой режим использования блочных шифров называют электронной кодовой книгой (ECB - electronic codebook). Его главный недостаток состоит в том, что одинаковые блоки исходного текста при шифровании дадут одинаковые же блоки шифр-текста - а это может существенно облегчить противнику задачу взлома. Поэтому режим ECB не рекомендуется использовать при шифровании текстов, по длине превышающих один блок - в таких случаях лучше воспользоваться одним из режимов, связывающих различные блоки между собой. По умолчанию в CryptoAPI блочные шифры используются в режиме сцепления блоков шифр-текста (CBC - cipher block chaining). В этом режиме при шифровании очередной блок исходного текста вначале комбинируется с предыдущим блоком шифр-текста (при помощи побитового исключающего ИЛИ), а затем полученная последовательность битов поступает на вход блочного шифра. Образующийся на выходе блок шифр-текста используется для шифрования следующего блока. Самый первый блок исходного текста также должен быть скомбинирован с некоторой последовательностью битов, но "предыдущего блока шифр-текста" еще нет; поэтому режимы шифрования с обратной связью требуют использования еще одного параметра - он называется инициализирующим вектором (IV - initialization vector).

Инициализирующий вектор должен генерироваться отдельно с помощью уже известной нам функции CryptGenRandom и, как и солт-значение, передаваться вместе с ключом в открытом виде. Размер IV равен длине блока шифра. Например, для алгоритма RC2, поддерживаемого базовым криптопровайдером Microsoft, размер блока составляет 64 бита (8 байтов).

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

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

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

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

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

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