Використання інструментів криптографії в Delphi-додатках (исходники), Різне, Програмування, статті

Захист інформації від несанкціонованого доступу і розповсюдження відіграє все більш важливу роль у сучасному житті. Завдання криптографії – забезпечити цей захист. Про те, чим можуть бути корисні досягнення цієї науки для програміста, і як їх використовувати, і буде присвячена ця стаття.


Обмовлюся, що тема, зачеплена в статті, дуже складна і об’ємна. Наведений тут матеріал – лише крапля в морі. Для тих, кого тема зацікавить, можу порекомендувати книги, наприклад:



  1. Брюс Шнайер – “Прикладна криптографія”.
  2. Щербаков Л.Ю. Домашев А.В. – “Прикладна криптографія”.

Основні поняття


Перш за все, введемо кілька понять і термінів, щоб надалі їх зміст не викликав у Вас питань або непорозуміння.


Відкритий текст – Власне, це і є та інформація, яку ми будемо намагатися захистити від несанкціонованого доступу. “Відкритий текст” – це зовсім не обов’язково саме текст, це також можуть бути двійкові дані, програмний код, і т.д.
Шифрований текст – Результат перетворення відкритого тексту, з використанням криптографічних алгоритмів і додаткового параметра (ключа) недоступний для сприйняття.
Шифрування – Процес створення шифрованого тексту при наявності відкритого тексту і ключа.
Дешифрування – Процес відновлення відкритого тексту із шифрованого за допомогою ключа.
Ключ – Параметр шифру, необхідний для шифрування і / або дешифрування.

Шифри поділяються на дві групи:


Говорячи про асиметричних шифри, необхідно згадати ще одну чудову можливість. Уявіть, що ви зашифруєте повідомлення не відкритим ключем одержувача, а своїм закритим ключем. Розшифрувати таке повідомлення можна за допомогою вашого відкритого ключа, тобто все навпаки. Тепер виходить, що розшифрувати може хто завгодно, а зашифрувати – тільки ви. Крім того, внести осмислене зміна в повідомлення (Наприклад, приписати нулик до грошовій сумі) ніхто сторонній не зможе. Таким чином, одержувач зможе автентифікувати відправника, тобто він буде впевнений, що відправник повідомлення саме ви і ніхто інший. Це називається електронно-цифровий підпис (ЕЦП) або просто цифровий підпис.


Якими б чудовими асиметричні алгоритми не були, у них є один істотний недолік: за швидкодією вони поступаються симетричним раз в сто і використовуються в основному тільки для шифрування невеликих повідомлень. Тому на практиці найчастіше застосовують схеми, що поєднують в собі всі достоїнства і симетричних і асиметричних алгоритмів, наприклад:



  1. Секретний ключ, за допомогою якого буде зашифровано повідомлення в даному сеансі зв’язку (цей ключ не рекомендують використовувати повторно і називають “сеансовий”) шифрується асиметричним алгоритмом за допомогою відкритого ключа одержувача.
  2. Повідомлення шифрується симетричним алгоритмом за допомогою сеансового ключа.
  3. Зашифрований сеансовий ключ і повідомлення відправляється одержувачу.
  4. Одержувач спочатку розшифровує сеансовий ключ з допомогою свого закритого ключа, а потім і саме повідомлення.

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


Хеш-функція – Це така функція, значення якої є незворотнім перетворенням вихідного значення. Іншими словами, нехай у нас є число A. Обчислимо Y = H (A). Функція H буде незворотною, якщо знаючи значення Y відновити A буде неможливо. Такому умові задовольняє, наприклад, найпростіша контрольна сума, однак до хеш-функцій є ще одна серйозна вимога: дуже складним завданням повинно бути знаходження такого числа B не рівного A, що H (B) також буде дорівнювати Y (такі випадки називаються колізіями). Число Y називають дайджестом або відбитком повідомлення.


Де це може стати в нагоді? Наприклад, хорошим рішенням буде зберігати в базі даних паролів не самі паролі, а їх відбитки, при введенні користувачем пароля, вираховувати його відбиток і порівнювати зі значенням в базі даних. Якщо зловмисник отримає доступ до цієї бази, то паролі він дізнатися не зможе, так як хеш-функція необоротна. Також він навряд чи зможе підібрати інший пароль з аналогічним відбитком.


А тепер повернемося до цифрових підписів. Як вже було сказано, підписувати ціле повідомлення нерозумно. У цифрового підпису головне не секретність самого повідомлення, а гарантія того, що відправник той за кого себе видає, і текст повідомлення не було змінено після підписання. Зазвичай надходять так: вираховується відбиток повідомлення (зазвичай він становить 16-64 байт), шифрується закритим ключем відправника і передається разом із самим повідомленням. Одержувач обчислює відбиток повідомлення, розшифровує підпис відкритим ключем відправника і порівнює отримані значення. Ця процедура називається верифікацією.


Популярні алгоритми


До цього ми говорили про якихось абстрактних алгоритмах, а тепер настав час назвати їх по іменах. Серед симетричних алгоритмів можна виділити алгоритм DES (розроблений фірмою IBM і затверджений в 1977 урядом США як офіційний стандарт. Блоковий алгоритм. Незважаючи на популярність, алгоритм вразливий, історії відомі випадки злому), 3-DES, який насправді є ні що інше, як потрійне шифрування DES трьома ключами, RC2 (блочний), RC4 (потоковий), IDEA (блочний). У кожного з них свої переваги і недоліки.


Серед асиметричних алгоритмів слід виділити RSA, названий на честь Рона Ривеста, Аді Шаміра і Олена Адельмана, що розробили алгоритм в 1977 році. Ідея алгоритму полягає в наступному: перемножити два числа набагато простіше, ніж розкласти твір на множники. Про це алгоритмі я розповім детальніше.



  1. Для початку потрібно згенерувати два великих простих числа p та q.
  2. Знайти n = pq.
  3. Вибрати число e (зазвичай близько 10 000) взаємно просте з phi = (p-1) (q-1), тобто числа e і phi не мають жодних спільних дільників, крім 1.
  4. Генерується число d таке, що ed = 1 (mod phi) – запис означає, що (ed-1) ділиться на phi.
  5. Числа n і е публікуються як відкритий ключ, а число d тримається в суворій таємниці – це закритий ключ. Числа p і q бажано або знищити, або також зберігати в таємниці.

Повідомлення зашифровується за формулою y = xe(Mod n), де x – вихідне повідомлення, а y – зашифроване. Розшифровується за допомогою закритого ключа d наступним чином: x = yd(Mod n). Надійність алгоритму полягає в тому, що для відновлення закритого ключа d необхідно знати числа p і q. Їх можна отримати, розклавши на множники число n, але якщо числа p і q достатньо великі, то це завдання стає практично нерозв’язною. На даний момент рекомендують вибирати p і q такі, щоб твір n було не коротше 1024 біт.


Ну, і серед алгоритмів хешування можна назвати наступні: MD4 (128-розрядний відбиток), MD5 (Розроблений в 1991 році, 128-розрядний відбиток, прийшов на зміну MD4, в 2004 році в алгоритмі виявлена ​​уразливість, дозволяє досить швидко знаходити колізії), SHA-1 (Розроблений в 1995 році, 160-розрядний відбиток, довгий час був найбільш популярним, однак на початку 2005 року з ним сталося те ж саме, що і з MD5. Брюс Шнайер заявив: “SHA-1 has been broken”), SHA-224, SHA-256, SHA-384, SHA-512.


CryptoAPI


Криптографічні функції є частиною операційної системи Windows, і звернеться до них можна за допомогою інтерфейсу CryptoAPI. Основні можливості доступні ще з Windows 95, але з часом вони розширювалися. Опис функцій CryptoAPI можна знайти в MSDN, Літературі [2] або в довідковому файлі до Delphi. Функції міститися в бібліотеках advapi32.dll і crypt32.dll. Їх можна імпортувати самостійно, а можна скористатися файлом Wcrypt2.pas, який додається до даної статті.


Підключення до криптопровайдери. Контейнери ключів

Перша функція, яку ми розглянемо, буде





function CryptAcquireContext(phProv       :PHCRYPTPROV;
pszContainer :LPAWSTR;
pszProvider :LPAWSTR;
dwProvType :DWORD;
dwFlags :DWORD) :BOOL; stdcall;

У більшості випадків, робота з криптографічними можливостями Windows починається з виклику саме цієї функції, яка виконує підключення до криптопровайдери і повертає його дескриптор в параметрі phProv. Криптопровайдер являє собою dll, незалежний програмний модуль, який фактично виконує криптографічні алгоритми. Криптопровайдери бувають різні і відрізняються складом функцій (Наприклад, деякі криптопровайдери обмежуються лише цифровими підписами), використовуваними алгоритмами (деякі шифрують алгоритмом RC2, інші – DES) та іншими можливостями. У кожній операційній системі свій склад криптопровайдерів, проте в кожній присутній Microsoft Base Cryptographic Provider v1.0. При виконанні функції CryptAcquireContext, необхідно вказати ім’я провайдера і його тип (відповідно в параметрах pszProvider і dwProvType). Тип провайдера визначає склад функцій і підтримувані криптоалгоритми, наприклад:


Тип PROV_RSA_FULL


Тип PROV_RSA_SIG


Microsoft Base Cryptographic Provider v1.0 відноситься до типу PROV_RSA_FULL і для цього типу використовується за умовчанням (якщо в параметрі pszProvider вказати nil). У параметрі pszContainer необхідно вказати ім’я контейнера ключів, який ми збираємося використовувати. Справа в тому, що кожен криптопровайдер містить базу даних, в якій зберігаються ключі користувачів. Ці ключі групуються в контейнерах. Зберігаються тільки ключові пари для асиметричних алгоритмів, сеансові ключі не зберігаються, тому що їх не рекомендують використовувати повторно. Таким чином, кожен контейнер має ім’я і містить по одному ключу (Точніше парі відкритий-закритий ключ) для цифрового підпису та обміну ключами (пам’ятаєте, я говорив, що через низький швидкодії асиметричні алгоритми використовуються в основному тільки для шифрування сеансових ключів і підпису хеша). В залежності від криптопровайдера, база даних може зберігатися у файлах, реєстрі або в будь-яких апаратних засобах, але це не впливає на роботу програміста з контейнерами ключів. Якщо в якості параметра pszContainer вказати nil, то буде використовуватися контейнер ключів, назва якого збігається ім’ям користувача, під яким був здійснений вхід в систему. Але так робити не рекомендується: справа в тому, що якщо два додатки використовує один і той же контейнер, одне з них може змінити або знищити ключі, необхідні для коректної роботи іншої програми. Тому рекомендують використовувати контейнери, імена яких збігається з ім’ям додатка.

Параметр dwFlags може бути нульовим або приймати одне з наступних значень:

CRYPT_VERIFYCONTEXT – цей прапор призначений для додатків, які не повинні мати доступ до закритих ключів контейнера. Такі програми можуть звертатися тільки до функцій хешування, перевірки цифрового підпису або симетричного шифрування. У цьому випадку параметр pszContainer повинен бути рівний nil.
CRYPT_NEWKEYSET – створює новий контейнер ключів, але самі ключі не створюються.
CRYPT_DELETEKEYSET – видаляє контейнер разом з зберігаються там ключами. Якщо заданий цей прапор, то підключення до криптопровайдери не відбувається і параметр phProv невизначений.
CRYPT_MACHINE_KEYSET – за умовчанням контейнери ключів зберігаються як користувацькі. Для основних криптопровайдерів це означає, що контейнери ключів зберігаються в призначених для користувача профілях. Цей прапор можна встановлювати в комбінації з іншими, щоб вказати, що контейнер є машинним, тобто зберігається в профілі All Users.

В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.






function CryptReleaseContext(hProv   :HCRYPTPROV;
dwFlags :DWORD) :BOOL; stdcall;

Звільняє контекст криптопровайдера і контейнера ключів. hProv – дескриптор криптопровайдера, отриманий при виклику CryptAcquireContext. dwFlags – зарезервований і має дорівнювати нулю.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.

Наведемо приклад роботи з цими функціями:






uses Wcrypt2;

procedure CryptProc;
var
Prov: HCRYPTPROV;
begin
CryptAcquireContext(@Prov,nil,nil,PROV_RSA_FULL,CRYPT_VERIFYCONTEXT); / / Працюємо з функціями CryptoAPI

CryptReleaseContext(Prov,0);
end;

Перш, ніж перейти безпосередньо до криптографічних функцій, згадаю ще про такі функції як CryptSetProvider, CryptGetDefaultProvider, CryptGetProvParam, CryptSetProvParam, CryptEmunProviders, CryptEnumProviderTypes, опис яких ви знайдете самі.

Хешування та електронно-цифровий підпис







function CryptCreateHash(hProv   :HCRYPTPROV;
Algid :ALG_ID;
hKey :HCRYPTKEY;
dwFlags :DWORD;
phHash :PHCRYPTHASH) :BOOL; stdcall;

Функція створює в системі хеш-об’єкт і повертає в параметрі phHash його дескриптор. Дані, що надходять на вхід хеш-об’єкта, там перетворюються, і їх відбиток зберігається всередині хеш-об’єкта.


У параметрі hProv потрібно вказати дескриптор провайдера, отриманий за допомогою CryptAcquireContext. Параметр Algid вказує на те, який алгоритм хешування буде використовуватися. Для Microsoft Base Cryptographic Provider може приймати такі значення: CALG_MAC, CALG_MD2, CALG_MD5, CALG_SHA. Сенс цих значень, думаю, зрозумілий. Параметр hKey детально розглядати не будемо, ви можете почитати про нього самі. Скажу лише, що звичайно (якщо не використовується алгоритм із секретним ключем, такий як MAC) його вказують рівним нулю. Параметр dwFlags зарезервований на майбутнє і має дорівнювати нулю.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.







function CryptDestroyHash(hHash :HCRYPTHASH) :BOOL; stdcall;

Функція знищує хеш-об’єкт, створений за допомогою CryptCreateHash. У параметрі hHash вказується дескриптор хеш-об’єкта.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.






function CryptHashData(hHash       :HCRYPTHASH;
const pbData :PBYTE;
dwDataLen :DWORD;
dwFlags :DWORD) :BOOL; stdcall;

Функція дозволяє додавати дані до об’єкта хеш-функції. Функція може викликатися кілька разів, дані, від яких ми обчислюємо хеш, розбиті на порції. У параметрі hHash вказується дескриптор хеш-об’єкта, створений за допомогою CryptCreateHash. pbData містить покажчик на дані, а dwDataLen містить розмір цих даних в байтах. Для Microsoft Base Cryptographic Provider параметр dwFlags повинен бути рівний нулю.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.






function CryptSignHash(hHash        :HCRYPTHASH;
dwKeySpec :DWORD;
sDescription :LPAWSTR;
dwFlags :DWORD;
pbSignature :PBYTE;
pdwSigLen :PDWORD) :BOOL; stdcall;

Функція обчислює значення електронно-цифрового підпису від значення хеша. У параметрі hHash вказується дескриптор хеш-об’єкта, створений за допомогою CryptCreateHash. dwKeySpec вказує, який ключ буде використаний для створення підпису. Як уже говорилося, в сховище ключів міститься дві ключові пари: для підпису і для обміну ключами. Відповідно цей параметр може приймати значення AT_SIGNATURE або AT_KEYEXCHANGE (логічніше використовувати AT_SIGNATURE). Ключі повинні існувати в контейнері. sDescription може містити довільну рядок опису. Цей рядок буде додана до хешу і повинна бути відома приймальній стороні. Використовувати цей параметр не рекомендується, так як це знижує безпеку системи. Параметр dwFlags не підтримується в Microsoft Base Cryptographic Provider і на його місці слід вказати нуль. pbSignature вказує на буфер, куди буде поміщена цифровий підпис, а pdwSigLen – розмір цього буфера. Якщо розмір заздалегідь не відомий, то можна вказати pbSignature рівним nil, і тоді у параметрі pdwSigLen ми отримаємо необхідний розмір буфера.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.







function CryptVerifySignature(hHash        :HCRYPTHASH;
const pbSignature :PBYTE;
dwSigLen :DWORD;
hPubKey :HCRYPTKEY;
sDescription :LPAWSTR;
dwFlags :DWORD) :BOOL; stdcall;

Функція здійснює перевірку цифрового підпису. hHash – дескриптор хеш-об’єкта, значення якого є відбитком повідомлення, підпис якого ми перевіряємо. pbSignature – покажчик на буфер, що містить підпис, dwSigLen – розмір цього буфера. hPubKey – дескриптор відкритого ключа, за допомогою якого ми будемо перевіряти підпис. Відкритий ключ повинен відповідати закритому, яким здійснювалася підпис. Про те, як отримати цей ключ, поговоримо пізніше. Параметри sDescription і dwFlags повинні відповідати параметрам функції CryptSignHash при здійсненні підпису.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.


Наведу приклад роботи з цими функціями, спрощено, без контролю можливих помилок (у реальних додатках на це не варто закривати очі):


1. Створення підпису





uses Wcrypt2;

function SignMessage(Message: String): String;
var
Prov: HCRYPTPROV;
Hash: HCRYPTHASH;
BufLen: DWORD;
begin
Result:=””;
CryptAcquireContext(@Prov,nil,nil,PROV_RSA_FULL,0);
CryptCreateHash(Prov,CALG_MD5,0,0,@Hash);
CryptHashData(Hash,PByte(Message),Length(Message),0);
BufLen:=0;
CryptSignHash(Hash,AT_SIGNATURE,nil,0,nil,@BufLen);
if BufLen>0 then begin
SetLength(Result,BufLen);
CryptSignHash(Hash,AT_SIGNATURE,nil,0,PByte(Result),@BufLen);
end;
CryptDestroyHash(Hash);
CryptReleaseContext(Prov,0);
end;

2. Перевірка підпису

У коді будуть втрачені деякі фрагменти, про які ми поговоримо пізніше.





function VerifySign(Message, Sign: String): Boolean;
var
Prov: HCRYPTPROV;
Hash: HCRYPTHASH;
PublicKey: HCRYPTKEY;
begin
CryptAcquireContext(@Prov,nil,nil,PROV_RSA_FULL,0);
CryptCreateHash(Prov,CALG_MD5,0,0,@Hash);
CryptHashData(Hash,PByte(Message),Length(Message),0); / / Тут повинен бути імпорт відкритого ключа для перевірки підпису

Result:=CryptVerifySignature(Hash,PByte(Sign),Length(Sign),
PublicKey,nil,0); / / Тут має бути знищення відкритого ключа

CryptDestroyHash(Hash);
CryptReleaseContext(Prov,0);
end;

Рекомендую ознайомитися самостійно з функціями CryptHashSessionKey, CryptGetHashParam і CryptSetHashParam.


Шифрування на основі даних користувача або пароля

Для шифрування в CryptoAPI використовуються симетричні алгоритми, ключ для яких може бути отриманий двома шляхами: випадковим чином або на основі яких-небудь даних користувача, наприклад пароля. Причому до останнього варіанту генерації ключа є одна важлива вимога: при використанні одних і тих же паролів повинні виходити ідентичні ключі. Така можливість передбачена в CryptoAPI.






function CryptDeriveKey(hProv     :HCRYPTPROV;
Algid :ALG_ID;
hBaseData :HCRYPTHASH;
dwFlags :DWORD;
phKey :PHCRYPTKEY) :BOOL; stdcall;

У параметрі hProv потрібно вказати дескриптор провайдера, отриманий за допомогою CryptAcquireContext. Algid – ідентифікатор алгоритму, для якого генерується ключ. Для Microsoft Base Cryptographic Provider може приймати такі значення: CALG_RC2 і CALG_RC4. Користувальницькі дані (пароль) попередньо хешіруются і дескриптор хеш-об’єкта передається у функцію як параметр hBaseData. Старші 16 біт параметра dwFlags можуть містити розмір ключа в бітах або бути нульовими (у цьому випадку буде створений ключ з розміром за замовчуванням). Молодші 16 біт можуть бути нульовими або приймати такі значення або їх комбінації: CRYPT_EXPORTABLE, CRYPT_CREATE_SALT, CRYPT_USER_PROTECTED, CRYPT_UPDATE_KEY. До перших двох ми ще повернемося, а зі змістом інших ви можете ознайомитися самостійно. У параметрі phKey повертається дескриптор створеного ключа.


В разі успіху, функція повертає true, у противному випадку – false. GetLastError поверне код помилки.


Коли ключ є, можна приступати безпосередньо до шифрування. Для цього нам знадобляться функції CryptEncrypt і CryptDecrypt.






function CryptEncrypt(hKey       :HCRYPTKEY;
hHash :HCRYPTHASH;
Final :BOOL;
dwFlags :DWORD;
pbData :PBYTE;
pdwDataLen :PDWORD;
dwBufLen :DWORD) :BOOL; stdcall;

У параметрі hKey передається дескриптор ключа, необхідний для шифрування. Цей ключ також визначає алгоритм шифрування. Параметр hHash використовується, якщо дані одночасно шифруються і хешіруются (шифруватися і хешірованних будуть вихідні дані). В цьому випадку в параметрі hHash передається дескриптор заздалегідь створеного хеш-об’єкта. Цю можливість зручно використовувати, якщо необхідно одночасно зашифрувати і підписати повідомлення. Інакше цей параметр слід встановити в нуль. Параметр Final слід встановити в true, якщо переданий у функцію блок даних є єдиним або останнім. В цьому випадку він буде доповнений до необхідного розміру. Параметр dwFlags не використовується в Microsoft Base Cryptographic Provider і на його місці слід вказати нуль. pbData – покажчик на буфер, в якому міститиметься дані для зашифрування. Зашіфрованиие дані містяться в той же буфер. pdwDataLen – розмір даних, які будуть зашифровані. dwBufLen – розмір вихідного буфера, для блокових шифрів може бути більше, ніж pdwDataLen. Дізнатися необхідний розмір, можна передавши в параметрі pbData nil, в параметрі pdwDataLen – розмір даних, які необхідно зашифрувати, а в параметрі dwBufLen – що завгодно, наприклад нуль. Після такого виклику, необхідний розмір буфера буде міститися в параметрі pdwDataLen (саме pdwDataLen, а не dwBufLen, трохи нелогічно, ну да ладно). Щоб не було плутанини, наведу простий приклад:






var
Message: String;
BufLen, DataLen: DWORD;

begin

Message:=”Hello World!”;
BufLen:=Length(Message);
DataLen:=Length(Message); / / Обчислюємо необхідний розмір вихідної буфера
CryptEncrypt(Key,0,true,0,nil,@BufLen,0); / / Виділяємо пам’ять для буфера і шифруємо
SetLength(Message,BufLen);
CryptEncrypt(Key,0,true,0,PByte(Message),@DataLen,BufLen);


Тепер, розглянемо функцію, яка дозволяє розшифрувати повідомлення.






function CryptDecrypt(hKey       :HCRYPTKEY;
hHash :HCRYPTHASH;
Final :BOOL;
dwFlags :DWORD;
pbData :PBYTE;
pdwDataLen :PDWORD) :BOOL; stdcall;

Навіть не буду детально описувати всі параметри – тут все очевидно. Скажу лише, що в параметр pdwDataLen потрібно передати число байт шіфротекста, а після виклику в нього буде поміщена довжина відкритого повідомлення. Якщо використовується параметр hHash, то дані після розшифровки хешіруются. Це зручно використовувати, якщо потрібно одночасно розшифрувати повідомлення і перевірити підпис.


Після того, як робота з ключем закінчена, необхідно звільнити дескриптор:






function CryptDestroyKey(hKey  :HCRYPTKEY) :BOOL; stdcall;

Якщо hKey відноситься до сеансовому ключу або імпортованого відкритому ключу (про це нижче), то дескриптор звільняється, а ключ знищується. Якщо hKey відноситься до пари відкритий / закритий ключ, то дескриптор звільняється, а ключова пара зберігається в контейнері ключів.


У доданому до статті архіві, ви знайдете демонстраційний приклад роботи з цими функціями.

Генерація випадкових ключів. Імпорт / експорт ключів

Тільки що ми розглянули випадок, коли для зашифровки і розшифровки повідомлення відправник та одержувач використовували пароль, відомий тільки їм. Зараз розглянемо інший: відправник генерує ключ випадково і передає його одержувач у зашифрованому вигляді разом з повідомленням. При цьому для шифрування сеансового ключа використовується відкритий ключ одержувача. А де відправник його візьме?


Як вже було сказано, при створенні ключового контейнера за допомогою функції CryptAcquireContext, ключі в контейнері не створюються, їх потрібно згенерувати окремо. Розглянемо функцію:







function CryptGenKey(hProv   :HCRYPTPROV;
Algid :ALG_ID;
dwFlags :DWORD;
phKey :PHCRYPTKEY) :BOOL; stdcall;

Функція призначена для генерації випадкових сеансових ключів та ключових пар. Параметри цієї функції аналогічні однойменним параметрам функції CryptDeriveKey, за винятком того, що Algid може також приймати значення AT_KEYEXCHANGE і AT_SIGNATURE. В цьому випадку будуть згенеровані ключові пари відповідно для обміну ключами і цифрового підпису. Створення нового ключового контейнера має виглядати приблизно так:






uses Wcrypt2;

var
Prov: HCRYPTPROV;
ExchangeKey, SignKey: HCRYPTKEY;
begin
CryptAcquireContext(@Prov,”My_Container”,nil,PROV_RSA_FULL,CRYPT_NEWKEYSET); / / Створюємо ключові пари
CryptGenKey(Prov,AT_KEYEXCHANGE,0,@ExchangeKey);
CryptGenKey(Prov,AT_SIGNATURE,0,@SignKey); / / Працюємо з функціями CryptoAPI
… / / Звільняємо дескриптори ключових пар. Самі ключі зберігаються в контейнері
CryptDestroyKey(SignKey);
CryptDestroyKey(ExchangeKey);
CryptReleaseContext(Prov,0);
end;


Створені таким чином ключові пари, згодом можна отримати з контейнера, скориставшись функцією






function CryptGetUserKey(hProv     :HCRYPTPROV;
dwKeySpec :DWORD;
phUserKey :PHCRYPTKEY) :BOOL; stdcall;

Параметр dwKeySpec може приймати два значення: AT_KEYEXCHANGE і AT_SIGNATURE, значення яких очевидні. Дескриптор ключа повертається в параметрі phUserKey.


Тепер відповімо на запитання, як відправник зможе передати одержувачу свою відкриту частину ключа.






function CryptExportKey(hKey       :HCRYPTKEY;
hExpKey :HCRYPTKEY;
dwBlobType :DWORD;
dwFlags :DWORD;
pbData :PBYTE;
pdwDataLen :PDWORD) :BOOL; stdcall;

Функція дозволяє експортувати ключ в двійковий буфер, який згодом можна буде зберегти файл і передати кому-небудь. У параметрі hKey повинен міститися дескриптор експортованого ключа. Експортувати можна не тільки відкриті ключі, а також ключові пари цілком і сеансові ключі. В останніх двох випадках, ключі та ключові пари повинні бути створені функціями CryptGenKey або CryptDeriveKey з параметрами dwFlags рівними CRYPT_EXPORTABLE. Відкриті ж ключі завжди експортуються. Сеансові ключі і ключові пари експортуються тільки в зашифрованому вигляді. Параметр hExpKey визначає ключ, яким вони будуть зашифровані. Якщо експортується відкрита частина ключа, то цей параметр слід встановити в нуль, якщо експортується ключова пара цілком, то тут зазвичай передають дескриптор сеансового ключа (зазвичай отриманий за допомогою CryptDeriveKey), яким пара буде зашифрована, якщо експортується сеансовий ключ, то зазвичай він шифрується відкритим ключем одержувача (зазвичай використовується ключ обміну, але ніхто не забороняє використовувати ключ підпису). Параметр dwBlobType визначає тип експортованого ключа і може приймати такі значення: SIMPLEBLOB – сеансовий ключ, PUBLICKEYBLOB – відкритий ключ, PRIVATEKEYBLOB – Ключова пара цілком. Існують і інші значення, але вони не підтримуються стандартним криптопровайдером. Параметр dwFlags для Microsoft Base Cryptographic Provider має дорівнювати нулю. pbData – Буфер, куди будуть скопійовані дані, pdwDataLen – розмір цього буфера. Якщо він заздалегідь не відомий, то можна вказати як параметр pbData nil, і в pdwDataLen буде отриманий необхідний розмір.

Ось приклад експорту відкритого ключа:





procedure ExportPublicKey(FileName: TFileName);
var
Prov: HCRYPTPROV;
SignKey: HCRYPTKEY;
Stream: TMemoryStream;
BufSize: DWORD;
begin
CryptAcquireContext(@Prov,”My_Container”,nil,PROV_RSA_FULL,0);
CryptGetUserKey(Prov,AT_SIGNATURE,@SignKey);
Stream:=TMemoryStream.Create;
CryptExportKey(SignKey,0,PUBLICKEYBLOB,0,nil,@BufSize);
Stream.SetSize(BufSize);
CryptExportKey(SignKey,0,PUBLICKEYBLOB,0,PByte(Stream.Memory),@BufSize);
Stream.SaveToFile(FileName);
Stream.Free;
CryptDestroyKey(SignKey);
CryptReleaseContext(Prov,0);
end;


Імпорт ключа здійснюється за допомогою функції






function CryptImportKey(hProv     :HCRYPTPROV;
pbData :PBYTE;
dwDataLen :DWORD;
hPubKey :HCRYPTKEY;
dwFlags :DWORD;
phKey :PHCRYPTKEY) :BOOL; stdcall;

Тут практично все зрозуміло. Поясню лише, що в параметрі hPubKey необхідно передати дескриптор ключа, яким буде розшифрований імпортований ключ. Якщо імпортується ключова пара цілком, то параметр dwFlags можна встановити в CRYPT_EXPORTABLE, тоді імпортована пара може бути згодом також експортована. У параметрі phKey повернеться дескриптор отриманого ключа. Якщо це ключова пара, то вона буде збережена в контейнері.

Ось приклад імпорту відкритого ключа:





function ImportPublicKey(FileName: TFileName): HCRYPTKEY;
var
Prov: HCRYPTPROV;
Stream: TMemoryStream;
begin
Stream:=TMemoryStream.Create;
Stream.LoadFromFile(FileName);
CryptImportKey(Prov,PByte(Stream.Memory),Stream.Size,0,0,@Result);
Stream.Free;
end;

Тепер, скориставшись цією інформацією, ви без зусиль зможете відновити пропущені фрагменти у функції перевірки цифрового підпису, описаної раніше.


Отже, як же передати співрозмовнику зашифроване повідомлення:


  1. Одержувач експортує свій відкритий ключ обміну в файл і передає його відправнику повідомлення.
  2. Відправник генерує випадковий сеансовий ключ і шифрує їм повідомлення.
  3. Відправник імпортує відкритий ключ обміну одержувача, експортує сеансовий ключ, шифруючи його отриманим ключем обміну (ключ обміну в параметрі hExpKey).
  4. Зашифроване повідомлення передається разом із зашифрованим сеансовий ключем – так званий цифровий конверт.
  5. Одержувач імпортує сеансовий ключ, розшифровуючи його своїм закритим ключем обміну (його можна отримати, викликавши CryptGetUserKey) і за допомогою сеансового ключа розшифровує повідомлення.

Говорячи про сеансових ключах, що використовуються в Microsoft Base Cryptographic Provider потрібно згадати про одну неприємності: до початку 2000 року діяла заборона на експорт програмного забезпечення, що використовує кошти “сильної криптографії” за межами США і Канади. З цієї причини в базовому криптопровайдери не підтримуються ключі для симетричних алгоритмів довжиною більше 40 біт. Ключі довжиною 56 біт дозволялося використовувати тільки закордонним відділенням американських компаній. Для алгоритмів RC2 і RC4 рекомендована довжина ключа повинна становити 128 біт, тому відсутню кількість біт заповнюється нулями або випадковими значеннями, які повинні передаватися відкрито. Надійність захисту через це, зрозуміло, сильно страждає. До складу Windows XP входить Microsoft Enhanced Cryptographic Provider, в якому цій проблеми немає, але при використанні базового криптопровайдера, необхідно доповнювати ключ до потрібної довжини, використовуючи т.зв. солт-значення (salt-values). Згенерувати salt-value і внести його в ключ можна кількома способами, але найпростіший і очевидний – при виклику CryptGenKey або CryptDeriveKey передати в параметрі dwFlags значення CRYPT_CREATE_SALT, приблизно так:






CryptGenKey(Prov,CALG_RC2,CRYPT_EXPORTABLE or CRYPT_CREATE_SALT,@Key);

При експорті ключа солт-значення не зберігається, про нього повинен подбати сам програміст.





var
SaltLen: DWORD;
Stream: TMemoryStream;

begin
… / / Визначаємо розмір буфера для солт-значення
CryptGetKeyParam(Key,KP_SALT,nil,@SaltLen,0); / / Зберігаємо його в файл
Stream:=TMemoryStream.Create;
Stream.SetSize(SaltLen);
CryptGetKeyParam(Key,KP_SALT,PByte(Stream.Memory),@SaltLen,0);
Stream.SaveToFile(“Salt.dat”);
Stream.Free;


Збережене таким чином солт-значення необхідно передати разом з сеансовий ключем, а на приймальному боці “вживити” його туди знову.





var
Stream: TMemoryStream;

begin

Stream:=TMemoryStream.Create;
Stream.LoadFromFile(“Salt.dat”);
CryptSetKeyParam(Key,KP_SALT,PByte(Stream.Memory),Stream.Size);
Stream.Free;


Для роботи з солт-значеннями ми скористалися функціями CryptGetKeyParam і CryptSetKeyParam, проте їх можливості на цьому не закінчуються. Рекомендую ознайомитися з ними самостійно, а також з іншими функціями, які в даній статті не згадувалися: CryptGenRandom, CryptDuplicateKey, CryptDublicateHash.


Інші корисності


До даної статті ще додаються деякі безкоштовні бібліотеки і програми, які не мають відношення до CryptoAPI, але також працюють із засобами криптографії і можуть бути дуже корисні:



  1. HashLib! 1.03 (C) Alex Demchenko, 2002, Moldova, Chishinev – дуже хороша і зручна бібліотека, в якій реалізовано безліч алгоритмів хешування: MD4, MD5, CRC32, HAVAL-128, SHA-1, SHA-256, TIGER-128, GOST, RIPEMD-128 та інші.
  2. FGInt copyright 2000, Walied Othman – відмінна бібліотека для роботи з гігантськими цілими числами, необхідними для роботи алгоритму RSA і з самим RSA.
  3. DCPCrypt Copyright (c) 1999-2003 David Barton – величезна бібліотека компонент, для роботи з криптографічними функціями.
  4. RSATool2v17 – Генератор чисел p, q, e, n, d для алгоритму RSA.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*