Работа с криптографией в EurekaLog

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

EurekaLog предлагает три модуля:

  1. EEncoding — содержит функции кодирования и преобразования данных;
  2. EHash — содержит функции хэширования;
  3. EEncrypt — содержит функции симметричного и ассиметричного шифрования.

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

Важное примечание: обновите EurekaLog до самой последней версии. Не все описываемые тут возможности доступны в предыдущих версиях, там некоторые функции были опубликованы специально для статьи.

Кодирование

Прежде, чем говорить о криптографии, нужно определиться с исходными данными. Например, пусть вы хотите получить MD5-хэш строки 'Привет'. Но что вы подадите на вход функции хэша? Байты $CF$F0$E8$E2$E5$F2 ('Привет' в ANSI/Windows-1251)? Байты $1F$04$40$04$38$04$32$04$35$04$42$04 ('Привет' в Unicode/UTF-16)? Байты $D0$9F$D1$80$D0$B8 ('Привет' в UTF-8)? В зависимости от того, как вы определите этот вопрос, вы получите разный результат. Например, MD5-хэш для строки 'Привет' в UTF-16 будет 8EFA2364EE560EE1B862ECC8D430C9AD, для 'Привет' в ANSI — это 43A3F987A7AF93811B7682E43ED0752A, а для 'Привет' в UTF-8 — 8A669E9418750C81AB90AE159A8EC410.

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

Поэтому, когда вам нужна точная определённость в результате, вы должны оперировать байтами, а не строками. В Delphi для оперирования с байтами вы можете:

  • Использовать указатель на данные + их размер: (const ABuffer: Pointer; const ABufferSize: Cardinal);
  • Использовать TBytes (array of Byte — динамический массив байтов);
  • Использовать RawByteString;
  • Использовать TStream и его наследников.

В частности, в EurekaLog функции принимают пару параметров указатель+размер, а также имеются перегруженные варианты, принимающие RawByteString.

К примеру, если вы попробуете вычислить MD5-хэш «просто» строки 'Привет' в PHP, вы получите 8a669e9418750c81ab90ae159a8ec410 — т.е. MD5-хэш от UTF-8 представления 'Привет'.

Откуда также можно сделать вывод, что строки в PHP хранятся в UTF-8; для сравнения: в Delphi строки хранятся как UTF-16 (начиная с Delphi 2009), либо в ANSI (Delphi 2007 и ранее).

Если вы захотите изменить кодировку в PHP — вам понадобится вызвать что-то типа mb_convert_encoding. А если вы захотите изменить кодировку в Delphi — вам понадобятся функции Delphi. В частности, для конвертации в/из UTF-8, TEncoding. В Delphi 2009 и выше вы также можете просто объявить строковый тип нужной кодировки и конвертирование данных строки будет выполнено автоматически при присвоении.

Сказанное верно и в обратную сторону: результатом вызова криптографических функций является набор байт (хэш, зашифрованные данные, и т.д.). Если вы захотите отобразить этот результат человеку — вам придётся конвертировать его в строку. Делать это можно, опять же, разными способами. К примеру, вы можете использовать встроенную функцию BinToHex или её более удобные аналоги HexEncodeString/HexEncodeToString из EurekaLog. Можно использовать Base64EncodeString/Base64EncodeToString из EurekaLog. Если, вдруг, вам нужно преобразовать данные из/в RawByteString, то в EurekaLog есть хэлперы RAWToString/RAWFromString. Также для небольших по размеру данных вы вполне можете захотеть загружать/сохранять их в файлы — для этого есть FileToString/StringToFile (из модуля ECompatibility).

Примеры использования упомянутых функций можно посмотреть ниже.

Хэширование

В EurekaLog есть функции для вычисления таких хэшей:

  • CRC16
  • CRC32
  • MD5
  • SHA-1
  • SHA-256
  • SDBM — это неплохая хэш-функция общего назначения с равномерным распределением, удобна для использования в качестве ключа/индекса в БД

Все функции хэширования имеют имя ИмяХэшаHash (например, MD5Hash()), возвращают результат типа TИмяХэшаHash (например, TSHA1Hash), а на вход принимают RawByteString, либо пару указатель+данные.

Также для некоторых хэшей в EurekaLog есть реализация алгоритма HMAC, который может использоваться для различных целей. Один из способов использования — аутентификация пользователя путём комбинации соли (salt) и пароля для получения хэша через HMAC. Функции для HMAC имеют вид ИмяХэшаHMAC (например, MD5HMAC()) и принимают пароль и соль.

Вот несколько примеров:

1. Считаем хэш строки:

uses
EEncoding, // для HexEncodeToString
EHash; // для MD5Hash

procedure TForm1.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
UTF8Str: UTF8String; // Байтовое представление
Hash: TMD5Hash; // Хэш
begin
// Определим исходные данные
S := 'Привет';

// Определим точное представление в байтах
// В данном случае - UTF-8
UTF8Str := UTF8Encode(S);
// (можно просто UTF8Str := S; в Delphi 2009 и выше)

// Вычислим хэш
Hash := MD5Hash(UTF8Str);

// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет '8A669E9418750C81AB90AE159A8EC410'
end;

2. Считаем хэш файла:

uses
EEncoding, // для HexEncodeToString
EHash, // для SHA256Hash
ECompatibility; // для FileToString

procedure TForm1.Button1Click(Sender: TObject);
var
Content: RawByteString; // Исходные байты файла
Hash: TSHA256Hash; // Хэш
begin
// Загружаем файл в двоичную строку
Content := FileToString(ParamStr(0));
// В Content будет 'MZP'#0#2#0#0#0...

// Вычислим хэш
Hash := SHA256Hash(Content);
Finalize(Content); // не обязательно

// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет что-то вроде 'FCF52FDC753E3797FE5EE4B5A7680E656D044D6BF7D97C408F0F7874492E43C2'
end;

3. Считаем хэш строки в произвольной кодировке:

uses
EEncoding, // для HexEncodeToString (и TEncoding для старых Delphi)
EHash; // для CRC32Hash

procedure TForm21.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
Encoding: TEncoding; // Кодировка для конвертирования в байты
Content: TBytes; // Байтовое представление
Hash: TCRC32Hash; // Хэш
begin
// Определим исходные данные
S := 'Привет';

// Получаем любую кодировку
Encoding := TEncoding.GetEncoding(866);
// Также можно:
// Encoding := TEncoding.UTF8;
// Encoding := TEncoding.Unicode;
// Encoding := TEncoding.ANSI;
try

// Конвертируем строку в байты
Content := Encoding.GetBytes(S);

finally
FreeAndNil(Encoding);
end;

// Вычислим хэш
Hash := CRC32Hash(Pointer(Content), Length(Content));
Finalize(Content); // не обязательно

// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет '6DB3A7B9'
// Также можно IntToStr(Hash) - что даст 3114775405
end;

4. Обмениваемся хэшем с PHP:

uses
EEncoding, // для HexEncodeToString
EHash, // для MD5Hash
ECore; // для ShellExec

procedure TForm1.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
UTF8Str: UTF8String; // Байтовое представление строки
Hash: TMD5Hash; // Хэш
HashStr: String; // Текстовое представление хэша
begin
// Определим исходные данные
S := 'Привет';

// Определим точное представление в байтах
// В данном случае - UTF-8
UTF8Str := UTF8Encode(S);
// (можно просто UTF8Str := S; в Delphi 2009 и выше)

// Вычислим хэш
Hash := MD5Hash(UTF8Str);

// Преобразуем двоичное представление в текст
HashStr := HexEncodeToString(@Hash, SizeOf(Hash));
// Хранит '8A669E9418750C81AB90AE159A8EC410'

// Передадим хэш в скрипт PHP
ShellExec(Format('http://localhost/test.php?hash=%s', [HashStr]));
end;
<?php

// Исходная строка (хранится в UTF-8)
$Source = 'Привет';

// Вычислим хэш исходной строки
// (функция вернёт строковое представление хэша)
$Hash = md5($Source);

// Прочитаем аргумент скрипта
$HashArg = $_GET['hash'];

// Проверим, совпадает ли хэш, сравнивая строковые представления
if (strtolower($Hash) == strtolower($HashArg)) {
// также можно использовать (начиная с PHP 5.6):
// if (hash_equals($HashArg, $Hash)) {

echo('OK'); // должны попасть сюда,
// т.е. исходные строки совпадают
// 'Привет' в Delphi = 'Привет' в PHP
} else {
echo('FAIL');
}

5. Хранение учётных данных в БД:

uses
EHash, // для SHA256HMAC
EEncrypt, // для InitSalt
EEncoding; // для RAWToString и HexEncodeToString/HexDecodeFromString

procedure TForm1.Button1Click(Sender: TObject);
var
UserName: String; // Пользовательский логин
UserPassword: String; // Пользовательский пароль
UserPasswordRAW: RawByteString; // Двоичное представление пароля
Salt: TSalt; // Соль (salt)
SaltStr: String; // Строковое представление соли
Hash: TSHA256Hash; // Хэш пароля
Hash2: TSHA256Hash; // Сохранённый хэш пароля
HashStr: String; // Строковое представление хэша
begin
// Шаг 1. Регистрация аккаунта

// Каким-то образом получаем логин и пароль
UserName := InputBox('Sign in', 'Enter the login:', '');
UserPassword := InputBox('Sign in', 'Enter the password:', '');

// Генерируем случайные байты для использования их в качестве соли
Salt := InitSalt;

// Преобразуем пароль в двоичное представление
UserPasswordRAW := UTF8Encode(UserPassword);

// Вычисляем хэш от соли и пароля через HMAC
Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW));

// Кодируем двоичные данные в текст
SaltStr := HexEncodeToString(@Salt, SizeOf(Salt));
HashStr := HexEncodeToString(@Hash, SizeOf(Hash));

// Вставляем в БД новую запись
// Это псевдо-код
InsertIntoDBTable('users', ['login', 'salt', 'password'], [UserName, SaltStr, HashStr]);
// Здесь:
// 'users' - имя таблицы
// 'login' - строковое поле произвольной длины
// 'salt' - строковое поле в 32 символа или двоичное поле в 16 байт
// 'password' - строковое поле в 64 символа или двоичное поле в 32 байта



// Шаг 2. Проверка логина

// Каким-то образом получаем логин и пароль
UserName := InputBox('Log in', 'Enter the login:', '');
UserPassword := InputBox('Log in', 'Enter the password:', '');

// Ищем в БД запись пользователя с введённым логином
// Это псевдо код, просто для примера
// В реальном коде нужно использовать параметры БД
Query := Format('SELECT salt, password FROM users WHERE login = ''%s'' LIMIT 1', [UserName]);
Values := DBQuery(Query);

// Если записи нет - введён неверный логин
if Length(Values) = 0 then
begin
ShowMessage('Invalid login');
Exit;
end;

// Преобразуем соль и хэш из текста в двоичные данные
SaltStr := Values[0]; // поле 'salt' из SELECT
HashStr := Values[1]; // поле 'password' из SELECT

Assert(HexCalcDecodedSize(Length(SaltStr)) = SizeOf(Salt));
HexDecodeFromString(SaltStr, @Salt);

Assert(HexCalcDecodedSize(Length(HashStr)) = SizeOf(Hash2));
HexDecodeFromString(HashStr, @Hash2);

// Повторим вычисления хэша, как и выше
UserPasswordRAW := UTF8Encode(UserPassword);
Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW));

// Здесь у нас есть:
// Hash - хэш введённого пароля
// Hash2 - хэш сохранённого пароля
// Если два хэша равны, то равны и пароли

// Проверяем, что пароль был введён верно, сравнивая хэши
if CompareMem(@Hash, @Hash2, SizeOf(Hash)) then
ShowMessage('OK')
else
ShowMessage('Invalid password');
end;
<?php

// Шаг 1. Регистрация аккаунта

// Каким-то образом получаем логин и пароль
$UserName = $_GET['login'];
$UserPassword = $_GET['password'];

// Генерируем случайные байты для использования их в качестве соли
$Salt = random_bytes(16);

// Вычисляем хэш от соли и пароля через HMAC
$HashStr = hash_hmac('sha256', $Salt, $UserPassword);

// Кодируем двоичные данные в текст
$SaltStr = bin2hex($Salt);

// Вставляем в БД новую запись
// Это псевдо-код
InsertIntoDBTable('users', ['login', 'salt', 'password'], [$UserName, $SaltStr, $HashStr]);
// Здесь:
// 'users' - имя таблицы
// 'login' - строковое поле произвольной длины
// 'salt' - строковое поле в 32 символа или двоичное поле в 16 байт
// 'password' - строковое поле в 64 символа или двоичное поле в 32 байта



// Шаг 2. Проверка логина

// Каким-то образом получаем логин и пароль
$UserName = $_GET['login'];
$UserPassword = $_GET['password'];

// Ищем в БД запись пользователя с введённым логином
// Это псевдо код, просто для примера
// В реальном коде нужно использовать параметры БД
$Query = 'SELECT salt, password FROM users WHERE login = '' . $UserName . '' LIMIT 1';
$Values = DBQuery($Query);

// Если записи нет - введён неверный логин
if (empty($Values)) {
echo('Invalid login');
die;
}

// Преобразуем соль и хэш из текста в двоичные данные
$SaltStr = Values['salt']; // поле 'salt' из SELECT
$HashStr2 = Values['password']; // поле 'password' из SELECT

$Salt = hex2bin($SaltStr);

// Повторим вычисления хэша, как и выше
$HashStr = hash_hmac('sha256', $Salt, $UserPassword);

// Здесь у нас есть:
// $HashStr - строковое представление хэша введённого пароля
// $HashStr2 - строковое представление хэша сохранённого пароля
// Если два хэша равны, то равны и пароли

// Проверяем, что пароль был введён верно, сравнивая хэши
if (strtolower($HashStr) == strtolower($HashStr2)) {
// также можно использовать (начиная с PHP 5.6):
// if (hash_equals($HashStr2, $HashStr)) {

echo('OK'); // должны попасть сюда,
// т.е. пароли совпали
} else {
echo('Invalid password');
}

Шифрование

В EurekaLog есть такие функции для шифрования:

  • Внутрипроцессное шифрование (например, для защиты паролей)
  • Межпроцессное шифрование (для защиты внешних данных в рамках пользовательского аккаунта или всего компьютера)
  • TEA
  • Twofish
  • RSA

Как и функции хэширования, функции шифрования принимают на вход указатель на данные + их размер, либо RawByteString. Но поскольку, в отличие от функций хэширования, функциям шифрования также нужно возвращать данные произвольного размера — вы также можете использовать запись TEncryptBuffer, которая просто объединяет указатель + размер в один параметр.

Внутрипроцессное шифрование

Иногда в приложении необходимо оперировать «секретной» информацией. Чтобы уменьшить риски утечки этой информации, необходимо хранить её в зашифрованном виде. Подробнее о такой практике можно почитать в MSDN или в (крайне рекомендую) книге. EurekaLog предоставляет функции для оперирования конфиденциальной информацией:

Функция MemProtect зашифровывает указанный блок памяти в процессе так, что расшифровать его можно только из этого же процесса. Функция MemUnprotect расшифровывает блок памяти, зашифрованный MemProtect. Функцию же SecureFree можно использовать для удаления почти чего угодно. Функция затрёт содержимое перед освобождением памяти.

Например:

uses
EEncrypt; // для MemProtect/MemUnprotect и SecureFree

procedure TForm1.Button1Click(Sender: TObject);
var
UserPassword: String;
StoredPassword: TEncryptBuffer;
ClearText: TEncryptBuffer;
begin
// Обнуляем все буферы
FillChar(StoredPassword, SizeOf(StoredPassword), 0);
FillChar(ClearText, SizeOf(ClearText), 0);

// Каким-то образом получаем конфиденциальную информацию
UserPassword := InputBox('Query', 'Enter the password:', '');
try

// Шифруем информацию
ClearText.pbData := Pointer(UserPassword);
ClearText.cbData := Length(UserPassword) * SizeOf(Char);
MemProtect(ClearText, StoredPassword);

finally
// Затираем оригинал в открытом виде
SecureFree(UserPassword);
// Нет необходимости удалять ClearText,
// поскольку мы не выделяли для него память
end;

// ...

// Далее у нас есть StoredPassword - информация в зашифрованном виде
// Каждый раз, когда нам надо использовать конфиденциальную информацию,
// мы должны расшифровать её, использовать, затем удалить расшифрованный вариант

// ...

// Расшифровываем информацию
MemUnprotect(StoredPassword, ClearText);
try

// Как-то используем конфиденциальную информацию
Hash := MD5Hash(ClearText.pbData, ClearText.cbData);

finally
// Затираем оригинал в открытом виде
SecureFree(ClearText);

// Также можно удалить производную информацию
SecureFree(Hash);
end;

// ...

// В конце работы - удаляем зашифрованную информацию
SecureFree(StoredPassword);
end;

Межпроцессное шифрование

Иногда возникает необходимость хранить конфиденциальную информацию. Например, опция «Запомнить меня» может сохранить пароль аккаунта в реестре. В этом случае функции MemProtect/MemUnprotect не помогут, поскольку они не работают между процессами (а перезапуск программы и чтение сохранённых данных — это новый процесс). Для таких случаев в EurekaLog есть похожие функции DataProtect и DataUnprotect. Например:

uses
EEncrypt, // для DataProtect/DataUnprotect и SecureFree
EConfig, // для RegKeyWrite/RegKeyRead
EEncoding; // для Base64EncodeString/Base64DecodeString

procedure TForm1.Button1Click(Sender: TObject);
var
UserPassword: String;
StoredPassword: RawByteString;
ClearText: RawByteString;
begin
// Каким-то образом получаем конфиденциальную информацию
UserPassword := InputBox('Query', 'Enter the password:', '');
try

// Конвертируем в RawByteString для удобства
ClearText := UTF8Encode(UserPassword);

// Затираем оригинал в открытом виде
SecureFree(UserPassword);

// Шифруем информацию
StoredPassword := DataProtect(ClearText);
// или:
// StoredPassword := DataProtect(ClearText, True);
// если вы хотите использовать HKEY_LOCAL_MACHINE ниже

// Затираем оригинал в открытом виде
SecureFree(ClearText);

// Кодируем зашифрованные двоичные данные в строку (текст)
UserPassword := Base64EncodeString(StoredPassword);

// Сохраняем зашифрованную конфиденциальную информацию в реестре
RegKeyWrite(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'SavedPassword', UserPassword);

// Опционально
SecureFree(UserPassword);

finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(UserPassword);
SecureFree(StoredPassword);
SecureFree(ClearText);
end;

// ...

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

// ...

// Читаем сохранённую зашифрованную конфиденциальную информацию
UserPassword := RegKeyRead(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'SavedPassword', '');
try

// Декодируем строку (текст) в зашифрованные двоичные данные
StoredPassword := Base64DecodeString(UserPassword);

// Опционально - удаляем прочитанные зашифрованные данные
SecureFree(UserPassword);

// Расшифровываем информацию
ClearText := DataUnprotect(StoredPassword);

// Опционально - удаляем прочитанные зашифрованные данные
SecureFree(StoredPassword);

// Поскольку ранее для удобства мы кодировали данные - нужно сделать обратную конвертацию
UserPassword := UTF8ToString(ClearText);

// Затираем оригинал в открытом виде
SecureFree(ClearText);

// Как-то используем конфиденциальную информацию
Hash := MD5Hash(Pointer(UserPassword), Length(UserPassword) * SizeOf(Char));

// Затираем оригинал в открытом виде
SecureFree(UserPassword);

finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(UserPassword);
SecureFree(StoredPassword);
SecureFree(ClearText);
end;
end;

Симметричное шифрование

Для симметричного шифрования EurekaLog поддерживает TEA и Twofish. Оба алгоритма шифрования не патентованы и могут быть свободно использованы в любой программе. TEA используется в широком спектре аппаратного обеспечения благодаря крайне низким требованиям к памяти и простоте реализации. Twofish — надёжный алгоритм симметричного шифрования общего назначения.

Обратите внимание, что зашифрованные данные могут быть больше по размеру чем исходные — поскольку симметричные алгоритмы шифрования часто оперируют блоками данных. Отличительной особенностью TEA является то, что зашифрованные данные будут равны по размеру исходным данным, поэтому у функций шифрования/расшифровки TEA есть перегруженный вариант для операций in-place, т.е. без перераспределения памяти. Для алгоритма Twofish размер данных должен быть кратен размеру блока (16 байт) — в противном случае данные будут дополнены (padding) алгоритмом PKCS#5 до минимально необходимого размера.

Для обоих алгоритмов EurekaLog предоставляет algEncrypt/algDecrypt функции, которые выглядят и работают одинаково. На вход функции принимают ключ шифрования и исходные данные. Разница есть только в том, что варианты для Twofish позволяют указать необязательный вектор инициализации (IV — initialization vector). Вектор инициализации — это просто случайный набор байт (который можно получить вызовом функции TwofishInitIV), который служит для того, чтобы два одинаковых блока данных шифровались бы по-разному, некий аналог соли (salt). Вектор инициализации не должен быть секретным и он передаётся (если есть) вместе с зашифрованными данными.

Оба алгоритма используют двоичные ключи для шифрования и расшифровки. Поскольку алгоритмы симметричные, то ключ для шифрования обязан совпадать с ключём для дешифрования. Разумеется, ключи для TEA и Twofish имеют разный (но фиксированный) размер в байтах. Как правило, ключи шифрования — не случайны, а получаются из паролей, вводимых пользователем. Чтобы преобразовать произвольный пароль в ключ фиксированной длины — используются функции algDeriveKey, на вход которым передаётся блок данных произвольного размера. «Вывод» ключа из пароля получается простым вызовом функции хэша от пароля с подходящим по размеру результатом. Например, для TEA это будет MD5, для Twofish — SHA-256. У derive-функций также есть перегруженный вариант, принимающий пароль в виде строки с опциональной солью (salt). В этом случае пароль конвертируется в UTF-8 представление, и хэш берётся от строки ‘соль’ + ‘UTF-8 пароль’. Кроме того, есть ещё вариант функции algDeriveKeyHMAC, который использует алгоритм HMAC для комбинации соли и пароля. В целом, если вы планируете использовать соль, то мы рекомендуем использовать функции algDeriveKeyHMAC.

Если ключи шифрования получаются как-то иначе (не из пароля) — вы также можете обмениваться ими напрямую, не «выводя» их из пароля. Просто обращайтесь с ключём, как с записью/набором байт фиксированного размера. Единственная тонкость — EurekaLog использует оптимизацию с Twofish: ключ используется не напрямую, а сначала преобразовывается в промежуточный вариант, что позволяет оптимизировать операции шифрования и расшифровки. Исходный ключ называется TTwofishRAWKey, а оптимизированный вариант — TTwofishKey.

Например:

1. Шифрование и расшифровка данных по паролю:

uses
EEncrypt; // для InitSalt, TEADeriveKey, TEAEncrypt/TEADecrypt, SecureFree

procedure TForm1.Button1Click(Sender: TObject);
var
Salt: TSalt; // Соль для преобразования пароля в ключ
Key: TTEAKey; // Ключ для шифрования и дешифрования
Source: String; // Исходный текст
SourceBytes: RawByteString; // Байтовое представление исходного текста
EncryptedBytes: RawByteString; // Зашифрованные исходные данные
begin
// Обнуляем все буферы
FillChar(Salt, SizeOf(Salt), 0);
FillChar(Key, SizeOf(Key), 0);

// Шаг 1: шифрование
try

// Определили исходные данные
Source := 'Привет';

// Определили байтовое представление исходных данных
// В данном случае - UTF-8
SourceBytes := UTF8Encode(Source);

// Исходные данные больше не нужны - затираем
SecureFree(Source);

// Создали случайный набор байтов для соли
Salt := InitSalt;

// Получили ключ из пароля
Key := TEADeriveKeyHMAC('супер секретный пароль', Salt);

// Зашифровали открытые данные
EncryptedBytes := TEAEncrypt(Key, SourceBytes);

// Исходные данные и ключ больше не нужны - затираем
SecureFree(Key);
SecureFree(SourceBytes);

finally
// На всякий случай (исключение) - затёрли и освободили всё
SecureFree(Source);
SecureFree(SourceBytes);
SecureFree(Key);
end;

// Далее у нас есть:
// Salt - соль для конвертации пароля в ключ
// EncryptedBytes - зашифрованные данные произвольного размера
// Именно эти данные нужно передать на сторону для расшифровки

// Шаг 2: расшифровка
try
// Получили ключ из пароля (Salt нам должны передать)
Key := TEADeriveKeyHMAC('супер секретный пароль', Salt);
// Здесь Key должен совпасть с ключом,
// который мы использовали для шифрования

// Расшифровали данные
SourceBytes := TEADecrypt(Key, EncryptedBytes);

// Удалили ключ
SecureFree(Key);

// Опционально
SecureFree(EncryptedBytes);

// Декодировали исходные данные
// В данном случае - из UTF-8
Source := UTF8ToString(SourceBytes);

// Исходные данные больше не нужны - затираем
SecureFree(SourceBytes);

// Как-то использовали исходные данные
ShowMessage(Source);

// Исходные данные больше не нужны - затираем
SecureFree(Source);

finally
// На всякий случай (исключение) - затёрли и освободили всё
SecureFree(Source);
SecureFree(SourceBytes);
SecureFree(EncryptedBytes);
SecureFree(Key);
SecureFree(Salt);
end;
end;

2. Обмен зашифрованными данными между Delphi и PHP:

uses
EEncrypt, // для всех Twofish функций и SecureFree
EEncoding, // для Base64EncodeToString
ECore; // для ShellExec

procedure TForm1.Button1Click(Sender: TObject);
const
// Секретный ключ, известный обоим сторонам
// Должен быть тем же самым, что и в PHP скрипте
// Это просто случайный набор байт, полученный вызовом TwofishInitSessionKeyRAW
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
SecretKey: TTwofishRAWKey =
(160, 22, 228, 9, 73, 192, 173, 149,
154, 19, 115, 215, 74, 36, 20, 202,
178, 26, 103 , 47, 51, 4, 144, 20,
73, 153, 49, 160, 192, 25, 20, 114);
var
Key: TTwofishKey; // Оптимизированный ключ
IV: TTwofishInitVector; // Вектор инициализации
Text: String; // Исходный текст
TextRAW: RawByteString; // Байтовое представление исходного текста
EncryptedText: RawByteString; // Двоичное представление зашифрованного текста
EncodedIV: String; // Кодированный в текст вектор инициализации
EncodedText: String; // Кодированный в текст зашифрованный текст
URL: String; // URL для вызова PHP
ReplyRAW: RawByteString; // Ответ от PHP (байты)
Reply: String; // Ответ от PHP (текст)
begin
// Готовим буферы
FillChar(Key, SizeOf(Key), 0);
FillChar(IV, SizeOf(IV), 0);
try

// Откуда-то получаем исходный текст
Text := 'Привет!';

// Конвертируем текст в байты. В данном случае - UTF-8
TextRAW := UTF8Encode(Text);

// Затрём исходный текст
SecureFree(Text);

// Оптимизируем ключ
Key := TwofishInitKey(SecretKey);

// Сгенерируем случайные байты для использования в качестве вектора инициализации
IV := TwofishInitIV;

// Шифруем открытый текст
// Поскольку используется вектор инициализации,
// то шифрование происходит в режиме CBC
EncryptedText := TwofishEncrypt(Key, TextRAW, @IV);

// Затрём исходный текст и ключ
SecureFree(TextRAW);

// Кодируем в текст двоичные данные
EncodedIV := Base64EncodeToString(@IV, SizeOf(IV));
EncodedText := Base64EncodeString(EncryptedText);

// Опционально
SecureFree(EncryptedText);

// Формируем URL для вызова PHP скрипта
// URLEncode нужно для экранирования символа '+'
// Если вместо Base64 вы будете кодировать в HEX, то URLEncode не нужно
URL := Format('http://localhost/test.php?iv=%s&text=%s', [URLEncode(EncodedIV), URLEncode(EncodedText)]);

// Опционально
SecureFree(EncodedIV);
SecureFree(EncodedText);

// Передаём в PHP
if not InitWebTools then
RaiseLastOSError;
try
ReplyRAW := InternetGet(URL, [], []);
finally
DoneWebTools;
end;

// PHP-скрипт ничего не вернул?
if ReplyRAW = '' then
Abort;

// Опционально
SecureFree(URL);

// Декодируем текст в байты
ReplyRAW := Base64DecodeString(Trim(String(ReplyRAW)));

// Расшифровываем ответ
TextRAW := TwofishDecrypt(Key, ReplyRAW, @IV);

// Удаляем ключ и вектор
SecureFree(Key);
SecureFree(IV);

// Опционально
SecureFree(ReplyRAW);

// Конвертируем двоичное представление в текст, в данном случае - UTF-8
Text := UTF8ToString(TextRAW);

// Зануляем ответ
SecureFree(TextRAW);

// Как-то используем ответ
ShowMessage(Text);
// Покажет:
// 'Hello from PHP: Привет'

// Зануляем ответ
SecureFree(Text);

finally
// На всякий случай (исключение) - затираем всё
SecureFree(Text);
SecureFree(TextRAW);
SecureFree(Key);
SecureFree(IV);
SecureFree(EncryptedText);
SecureFree(EncodedIV);
SecureFree(EncodedText);
SecureFree(URL);
SecureFree(ReplyRAW);
SecureFree(Reply);
end;
end;
<?php

// Эти функции необходимы, потому что MCrypt использует дополнение нулями вместо PKCS#5
// OpenSSL поддерживает PKCS#5, но не поддерживает Twofish

// Дополнение данных по PKCS#5
function pkcs5_pad($text, $blocksize = 16) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}

// Обрезка PKCS#5
function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}

// Секретный ключ, известный обоим сторонам
// Должен быть тем же самым, что и в Delphi
// Это просто случайный набор байт, полученный вызовом random_bytes(32)
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
$Key = pack('C*',
160, 22, 228, 9, 73, 192, 173, 149,
154, 19, 115, 215, 74, 36, 20, 202,
178, 26, 103 , 47, 51, 4, 144, 20,
73, 153, 49, 160, 192, 25, 20, 114);

// Читаем переданные данные (вектор инициализации и зашифрованный текст)
$EncodedIV = $_GET['iv'];
$EncodedText = $_GET['text'];

// Декодируем текст в байты
$IV = base64_decode($EncodedIV);
$EncryptedText = base64_decode($EncodedText);

// Расшифровываем текст (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$Text = mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedText, MCRYPT_MODE_CBC, $IV);

// Обрезаем данные до их реального размера
$Text = pkcs5_unpad($Text);

// Что-то делаем с открытым текстом из Delphi
$Text = 'Hello from PHP: ' . $Text;

// Дополняем данные до размера, кратного блоку шифра
$Text = pkcs5_pad($Text);

// Зашифровываем ответ (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$EncryptedText = mcrypt_encrypt(MCRYPT_TWOFISH, $Key, $Text, MCRYPT_MODE_CBC, $IV);

// Кодируем зашифрованные байты в текст
$EncodedText = base64_encode($EncryptedText);

// Возвращаем ответ в Delphi
echo($EncodedText);

3. Хранение файла в зашифрованном виде:

uses
EEncrypt, // для всех Twofish функций и SecureFree
EEncoding, // для HexEncodeToString/HexDecodeString
EConfig, // для RegKeyWrite/RegKeyRead
ECompatibility; // для FileToString/StringToFile

procedure TForm1.Button1Click(Sender: TObject);
var
RAWKey: TTwofishRAWKey; // Ключ для шифрования и дешифрования (исходный)
Key: TTwofishKey; // Ключ для шифрования и дешифрования (оптимизированный)
IV: TTwofishInitVector; // Вектор инициализации
Content: RawByteString; // Исходные байты файла
EncryptedData: RawByteString; // Зашифрованный файл
DataClear: RawByteString; // Для шифрования ключа шифрования
DataEncrypted: RawByteString; // Для шифрования ключа шифрования
DataStr: String; // Строковое представление зашифрованного ключа шифрования
begin
// Обнуляем все буферы
FillChar(RAWKey, SizeOf(RAWKey), 0);
FillChar(Key, SizeOf(Key), 0);
FillChar(IV, SizeOf(IV), 0);

// Шаг 1: шифрование файла случайным ключём
try

// Создали случайный ключ
RAWKey := TwofishInitSessionKeyRAW;

// Оптимизировали ключ
Key := TwofishInitKey(RAWKey);

// Загружаем файл в двоичную строку
Content := FileToString(ParamStr(0));
// В Content будет 'MZP'#0#2#0#0#0...

// Создали случайный набор байтов для использования в качестве вектора
IV := TwofishInitIV;

// Шифруем данные (файл)
EncryptedData := TwofishEncrypt(Key, Content, @IV);

// Чистим и удаляем ненужные данные
SecureFree(Content);
SecureFree(Key);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(Content);
SecureFree(Key);
end;

// Далее у нас есть:
// RAWKey - (секретный) ключ, которым мы шифровали данные
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные

// Шаг 2: защита ключа шифрования
try

// Конвертируем ключ шифрования в RawByteString для удобства
DataClear := RAWToString(@RAWKey, SizeOf(RAWKey));

// Чистим исходный ключ шифрования
SecureFree(RAWKey);

// Шифруем ключ шифрования
DataEncrypted := DataProtect(DataClear);

// Чистим исходный ключ шифрования
SecureFree(DataClear);

// Преобразовали зашифрованный ключ в текст
DataStr := HexEncodeToString(Pointer(DataEncrypted), Length(DataEncrypted));

// Опционально
SecureFree(DataEncrypted);

// Сохранили зашифрованный ключ в реестре
// Мы можем это сделать, поскольку ключ сможет расшифровать только этот же пользователь
RegKeyWrite(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'EncryptionKey', DataStr);

// Опционально
SecureFree(DataStr);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RAWKey);
SecureFree(DataClear);
SecureFree(DataEncrypted);
SecureFree(DataStr);
end;

// Далее у нас есть:
// Зашифрованный (секретный) ключ в реестре
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные
// Если нам нужно сохранить зашифрованные данные на диск, то мы сохраняем IV и EncryptedData
// Расшифровать данные сможет только текущий пользователь и никто иной

// Шаг 3: расшифровка ключа шифрования
try

// Прочитали зашифрованный ключ шифрования (в виде текста)
DataStr := RegKeyRead(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'EncryptionKey', '');

// Декодировали текст в двоичные данные
DataEncrypted := HexDecodeString(DataStr);

// Опционально
SecureFree(DataStr);

// Расшифровали зашифрованный ключ
DataClear := DataUnprotect(DataEncrypted);

// Опционально
SecureFree(DataEncrypted);

// Подготовили расшифрованный ключ
Assert(Length(DataClear) = SizeOf(RAWKey));
RAWFromString(DataClear, @RAWKey);

// Удалили копию расшифрованного ключа
SecureFree(DataClear);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(DataStr);
SecureFree(DataEncrypted);
SecureFree(DataClear);
end;

// Далее у нас есть:
// RAWKey - (секретный) ключ, которым мы шифровали данные
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные

// Шаг 4: расшифровываем файл
try

// Оптимизировали ключ
Key := TwofishInitKey(RAWKey);

// Чистим ненужный ключ
SecureFree(RAWKey);

// Расшифровываем данные (файл)
// Здесь Key и IV должны быть теми же самыми, что использовались для шифрования
Content := TwofishDecrypt(Key, EncryptedData, @IV);

// Чистим ненужный ключ
SecureFree(Key);

// Опционально
SecureFree(EncryptedData);

// Сохранили расшифрованные данные в файл
StringToFile(ParamStr(0) + '.copy', Content);
// Здесь файл Project1.exe.copy получится в точности равным Project1.exe

// Затрём исходные данные
SecureFree(Content);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RAWKey);
SecureFree(Key);
SecureFree(EncryptedData);
SecureFree(Content);
end;
end;

Асимметричная криптография

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

Управление ключами

В EurekaLog ключи RSA хранятся в записи TRSAKey, которая имеет два поля: PublicKey для хранения открытого ключа и PrivateKey для хранения закрытого (секретного) ключа. Новую пару ключей можно сгенерировать функцией RSAGenKey (выполняется очень долго; читай: 5-15 секунд). Как правило, ключи не генерируются в программах, а загружаются уже готовые (заранее сгенерированные). Для сохранения загрузки ключей в EurekaLog есть функции RSALoad/SavePublic/PrivateKey, например: RSALoadPrivateKey. Для импорта/экспорта EurekaLog поддерживает несколько форматов, описываемых TRSAExport/TRSAImport:

  • rsBLOB — это двоичное представление ключа с заголовком. В качестве заголовка используются структуры PUBLICKEYBLOB/PRIVATEKEYBLOB Microsoft. Little-Endian.
  • rsDER — это двоичное представление ключа, кодированное в контейнер ASN.1 без опционального заголовка (формат PKCS#1), Big-Endian. При загрузке опциональный заголовок ASN.1 от PKCS#8 (например, после экспорта из PHP/OpenSSL) будет пропущен. В CryptoAPI соответствует RSA_CSP_PUBLICKEYBLOB/PKCS_RSA_PRIVATE_KEY. Как правило, такой формат сохраняется в файлы с расширением .der.
  • rsPEM — это текстовое представление ключа. По сути, это тот же rsDER, но кодированный в Base64. Как правило, такой формат сохраняется в файлы с расширением .pem, .key, .cert, .cer или .crt. Файлы в формате PKCS#1 используют заголовки -----BEGIN/END RSA PRIVATE KEY-----, -----BEGIN/END RSA PUBLIC KEY-----, а файлы в формате PKCS#8 используют заголовки -----BEGIN/END PRIVATE KEY-----, -----BEGIN/END PUBLIC KEY-----.

Зашифрованные ключи PKCS#1/PKCS#8 в EurekaLog не поддерживаются. Форматы контейнеров PKCS#7 и PKCS#12 в EurekaLog не поддерживаются. PKCS#8 поддерживается EurekaLog только на импорт (см. ниже).

Например, один и тот же публичный ключ экспортируется следующим образом:

  • rsBLOB (заголовок PUBLICKEYBLOB/PRIVATEKEYBLOB + little-endian, ключ $25 $17 $B4 $A0 … $96 $B9 $9C $E7 начинается с $15/21-го байта и до конца):
    rsBLOB
  • rsDER (ASN.1 контейнер PKCS#1 + big-endian, т.е. тот же самый ключ $E7 $9C $B9 $96 … $A0 $B4 $17 $25 начинается с 10-го байта и заканчивается на 6-м байте с конца):
    rsPEM
  • rsPEM (Base64-кодированный контейнер ASN.1 PKCS#1):
    -----BEGIN RSA PUBLIC KEY-----
    MIICCgKCAgEA55y5ll1KryRC7umxntWX7t3zOP3qUVxQo7gin3sA1dePyzLxTxtE
    47R+/sqkgFygXdlBqnmjbwu60kU2Zd7k7QFGhZWqfPcAYI3xd660vUPnmXK7n2R1
    3AtF2BW/5MqIH7D3ddjLCt5CoUn6KRZSuz+pySDpuquKerRB5Gq/0WjUG2IIcQXU
    Z1i4qMicPhbOJH76rFPgRngBuvJtS0UCBKx4YOlK0q1JUUJ1leSGp2gAjYGrD7fN
    SOU8r70a97NDu4UblmsS9zW29OHAEF7jNFsVNVBU78P/XZ4hmL41gaPRGws3HXfA
    vGbVattUzHTHsHMJeRLoiPAgak3TqAM2px7qOcNNN8FB91XbnxzvPARfDrBMbpc4
    OcWmDSMuc1RGI/mQCIlGRvA2nhD7Dfu3L5sxnrjjOC+LLpIVsGe5+cs1ZkfD7kII
    AzV/MXXNlx366n/Z1+u97VocmvHcqVCl/s9AMqdXflzAYD+9p7bXhJdP9XfOXf9z
    zCyPBK/Iyk+B4lRR9cmuBW7FAq1JM3PZWZ2mEx0fgrL8M0w5cf2Ts84XtNIEFDa6
    MFOe48sJfDIiPPw4ePohSuYpAY71Du2cQe87VQAf/caclWsrFplItilN93Xx5kQW
    5S16HHLc7A+EKEaNBnUsNl+n0/99jjfHA9PAqxFVaVT68X9eSKC0FyUCAwEAAQ==
    -----END RSA PUBLIC KEY-----

Пример создания и экспорта нового ключа:

  1. На Delphi (PKCS#1):
    uses
    EEncrypt; // для RSA функций

    procedure TForm1.Button1Click(Sender: TObject);
    var
    Key: TRSAKey;
    begin
    // Создали новую пару ключей (несколько секунд)
    Key := RSAGenKey;
    try

    // Экспорт обоих ключей во все форматы (для примера)
    RSASavePublicKey(Key, 'C:Documentspublic.blob', rsBLOB);
    RSASavePublicKey(Key, 'C:Documentspublic.der', rsDER);
    RSASavePublicKey(Key, 'C:Documentspublic.pem', rsPEM);

    RSASavePrivateKey(Key, 'C:Documentsprivate.blob', rsBLOB);
    RSASavePrivateKey(Key, 'C:Documentsprivate.der', rsDER);
    RSASavePrivateKey(Key, 'C:Documentsprivate.pem', rsPEM);

    finally
    // Удалили ключи
    SecureFree(Key);
    end;
    end;
  2. На PHP (PKCS#8):
    <?php

    // Создали новую пару ключей (выполняется несколько секунд)
    $config = array(
    "private_key_bits" => 4096,
    "private_key_type" => OPENSSL_KEYTYPE_RSA,
    "encrypt_key" => false
    );
    $privateKey = openssl_pkey_new($config);
    $publicKey = openssl_pkey_get_public($privateKey);

    // Сохраняем ключи в файлы
    openssl_pkey_export($privateKey, $PEM, null, $config);
    file_put_contents('./private.pem', $PEM);
    file_put_contents('./public.pem', $publicKey['key']);
  3. На OpenSSL (PKCS#8):
    openssl genpkey -out private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096
    openssl rsa -in private.pem -out public.pem -outform pem -pubout

Если вы хотите обмениваться ключами между Delphi/EurekaLog, Windows/WinCrypt и PHP/OpenSSL — вам нужно использовать формат с кодированием в контейнер ASN.1 (т.е. двоичный DER или текстовый PEM). Здесь есть один подводный камень:

  1. Windows/WinCrypt экспортируют/импортируют только сами ключи в контейнер ASN.1. Этот формат называется PKCS#1, в текстовом виде (PEM) его можно отличить по комментарию вида -----BEGIN RSA PUBLIC KEY-----. Если вы откроете сохранённый PEM/DER файл в любом ASN.1 редакторе/декодере (или используете команду openssl asn1parse -in private.pem), то увидите такую картину:
    SEQUENCE (9 elem)
    INTEGER 0
    INTEGER (4096 bit) 758666102228921792910938751013886686183781000609266742264329283135491…
    INTEGER 65537
    INTEGER (4096 bit) 626038158727742962915964533848528190012037116635627896022932505790124…
    INTEGER (2048 bit) 265173868639039499374763037151257212413358779720904507462546669893261…
    INTEGER (2048 bit) 286101381754789259717271171537486143896554619163648315398099842455950…
    INTEGER (2048 bit) 241273140164272477344356926702923044634459679795497746005945617372402…
    INTEGER (2047 bit) 117977475958972484914769571551956345862709440207784850140129213152449…
    INTEGER (2047 bit) 114103066753170488160676998988478007480145103422326665392933549037964…

    Здесь 8 полей описывают различные компоненты ключа (модуль, экспоненту и т.д.).

  2. PHP/OpenSSL перед ключом добавляют заголовок с указанием алгоритма. Этот формат называется PKCS#8, в текстовом виде (PEM) его можно отличить по комментарию вида -----BEGIN PRIVATE KEY-----. Если вы откроете сохранённый файл PEM/DER в любом ASN.1 редакторе/декодере, то увидите такую картину:
    SEQUENCE (3 elem)
    INTEGER 0
    SEQUENCE (2 elem)
    OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1)
    NULL
    OCTET STRING (2349 byte) 30820929020100028202010098D61F2FBA3EC958DB082F286781EE7CC258ADCE2B0A…
    SEQUENCE (9 elem)
    INTEGER 0
    INTEGER (4096 bit) 758666102228921792910938751013886686183781000609266742264329283135491…
    INTEGER 65537
    INTEGER (4096 bit) 626038158727742962915964533848528190012037116635627896022932505790124…
    INTEGER (2048 bit) 265173868639039499374763037151257212413358779720904507462546669893261…
    INTEGER (2048 bit) 286101381754789259717271171537486143896554619163648315398099842455950…
    INTEGER (2048 bit) 241273140164272477344356926702923044634459679795497746005945617372402…
    INTEGER (2047 bit) 117977475958972484914769571551956345862709440207784850140129213152449…
    INTEGER (2047 bit) 114103066753170488160676998988478007480145103422326665392933549037964…

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

Разные библиотеки поддерживают разные форматы. Если вы попробуете импортировать ключ, сохранённый в неподдерживаемом формате, то можете получить ошибки вроде таких:

CRYPT_E_ASN1_BADTAG (8009310B): ASN1 bad tag value met
openssl_pkey_get_private: error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag

EurekaLog сохряняет в PKCS#1, но умеет загружать PKCS#1 и PKCS#8. OpenSSL сохраняет в PKCS#8, но умеет загружать PKCS#8 и PKCS#1.

ВАЖНО
OpenSSL поддерживает PKCS#1/PKCS#8 в PEM только при наличии правильного комментария! Т.е. файл PKCS#1 должен начинаться с -----BEGIN RSA PRIVATE KEY----- или -----BEGIN RSA PUBLIC KEY-----, а файл PKCS#8 должен начинаться с -----BEGIN PRIVATE KEY----- или -----BEGIN PUBLIC KEY-----.

P.S. Вы можете сконвертировать PKCS#1 (Windows/WinCrypt/EurekaLog) в PKCS#8 (PHP/OpenSSL):

openssl pkcs8 -topk8 -inform pem -in private.pem -outform pem -nocrypt -out private2.pem
openssl rsa -RSAPublicKey_in -in public.pem -pubout -out public2.pem

и наоборот:

openssl rsa -inform pem -in private.pem -outform pem -out private2.pem
openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public2.pem

P.P.S. В последних версиях OpenSSL вы также можете использовать двоичный формат BLOB:
Преобразовать PEM в BLOB:

openssl rsa -inform PEM -in private.pem -outform "MS PRIVATEKEYBLOB" -out private.blob

Преобразовать BLOB в PEM:

openssl rsa -inform "MS PRIVATEKEYBLOB" -in private.blob -outform PEM -out private.pem

Для публичного ключа нужно поменять формат на "MS PUBLICKEYBLOB", либо вы можете конвертировать закрытый ключ, а потом получить публичный ключ из закрытого.

Суммируя вышесказанное, мы рекомендуем:

  1. Использовать утилиту EurekaLog Crypto Helper для создания пары RSA ключей:
    1. Запустить Пуск / Программы / EurekaLog / Tools / EurekaLog Crypto Helper
    2. Перейти на вкладку Keys
    3. Перейти на вкладку RSA
    4. Нажать кнопку Create New
    5. Сохранить закрытый и открытый ключи в файлы private.pem и public.pem соответственно. Это сохранит ключи в формате PKCS#1.

    Альтернативно, вы можете использовать такой код:

    uses
    EEncrypt; // для RSA функций

    procedure TForm1.Button1Click(Sender: TObject);
    var
    Key: TRSAKey;
    begin
    // Создали новую пару ключей (несколько секунд)
    Key := RSAGenKey;
    try

    // Экспорт обоих ключей в PKCS#1
    RSASavePublicKey (Key, 'C:Documentspublic.pem', rsPEM);
    RSASavePrivateKey(Key, 'C:Documentsprivate.pem', rsPEM);

    finally
    // Удалили ключи
    SecureFree(Key);
    end;
    end;
  2. Импортировать ключи в Delphi (EurekaLog умеет загружать PKCS#1):
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#1 PEM
    RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#1 PEM
    RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
  3. Импортировать ключи в PHP (OpenSSL в PHP умеет работать с PKCS#1):
    <?php

    $PrivateKey = <<<EOD
    -----BEGIN RSA PRIVATE KEY-----
    MIIJKgIBAAKCAgEA5CBBmb8QlDVt7uqKZPW6I/GcjyzDg+7VuZd5OcQXVpglsWoa
    ...
    aNz1gCLcqrQiTXHTVg821kYszBDySjfQGJQ3JJhf1/9XGcVjcopbWWeeNpHs5w==
    -----END RSA PRIVATE KEY-----
    EOD;

    // Или:
    // $PrivateKey = 'file:///var/www/private.pem';

    $PrivateKey = openssl_pkey_get_private($PrivateKey);
    if (!PrivateKey) {
    echo('openssl_pkey_get_private: ' . openssl_error_string());
    die();
    }
    <?php

    $PublicKey = <<<EOD
    -----BEGIN RSA PUBLIC KEY-----
    MIICCgKCAgEA5CBBmb8QlDVt7uqKZPW6I/GcjyzDg+7VuZd5OcQXVpglsWoauYvq
    ...
    f69nl8KyfHhsqffkDeDIaA73hspgFM5bh2zGdj4n8101bjHRu8N35qECAwEAAQ==
    -----END RSA PUBLIC KEY-----
    EOD;

    // Или:
    // $PublicKey = 'file:///var/www/public.pem';

    $PublicKey = openssl_pkey_get_public($PublicKey);
    if (!PublicKey) {
    echo('openssl_pkey_get_public: ' . openssl_error_string());
    die();
    }

    Здесь важно, чтобы заголовки были указаны правильно. Т.е. должно быть -----BEGIN RSA PRIVATE KEY----- и -----BEGIN RSA PUBLIC KEY-----.

Альтернативно, вы можете:

  1. Скачать OpenSSL для Windows.
  2. Создать пару ключей, эти команды создадут два файла private.pem и public.pem в формате PKCS#8 PEM:
    openssl genpkey -out private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096
    openssl rsa -in private.pem -out public.pem -outform pem -pubout
  3. Импортировать ключи в Delphi (EurekaLog умеет загружать PKCS#8):
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#8 PEM
    RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#8 PEM
    RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
  4. Импортировать ключи в PHP (OpenSSL в PHP умеет работать с PKCS#8):
    <?php

    $PrivateKey = <<<EOD
    -----BEGIN PRIVATE KEY-----
    MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u
    ...
    X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK
    N9AYlDckmF/X/1cZxWNyiltZZ542kezn
    -----END PRIVATE KEY-----
    EOD;

    // Или:
    // $PrivateKey = 'file:///var/www/private.pem';

    $PrivateKey = openssl_pkey_get_private($PrivateKey);
    if (!PrivateKey) {
    echo('openssl_pkey_get_private: ' . openssl_error_string());
    die();
    }
    <?php

    $PublicKey = <<<EOD
    -----BEGIN PUBLIC KEY-----
    MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5CBBmb8QlDVt7uqKZPW6
    ...
    v4sIP6NMmNKN8TwtqUKxcjZJMrVhjPJWf69nl8KyfHhsqffkDeDIaA73hspgFM5b
    h2zGdj4n8101bjHRu8N35qECAwEAAQ==
    -----END PUBLIC KEY-----
    EOD;

    // Или:
    // $PublicKey = 'file:///var/www/public.pem';

    $PublicKey = openssl_pkey_get_public($PublicKey);
    if (!PublicKey) {
    echo('openssl_pkey_get_public: ' . openssl_error_string());
    die();
    }

    Здесь важно, чтобы заголовки были указаны правильно. Т.е. должно быть -----BEGIN PRIVATE KEY----- и -----BEGIN PUBLIC KEY-----.

Примечание: вызов openssl_pkey_get_private/openssl_pkey_get_public является опциональным. Если переменная ключа содержит сам ключ в формате PEM, то его можно передавать напрямую в функции OpenSSL. В данном случае функции openssl_pkey_get_private/openssl_pkey_get_public вызываются только для примера и проверки правильности указания ключа.

Ассиметричное шифрование

Для шифрования и расшифровки в EurekaLog есть функции RSAEncrypt и RSADecrypt, которые используются аналогично функциям симметричного шифрования выше. Есть только несколько отличий:

  1. Поскольку асимметричное шифрование использует два разных ключа, оно используется в сценариях «отправитель-получатель» и не используется, когда шифрует и расшифровывает данные одно и то же лицо.
  2. Так как асимметричное шифрование работает очень медленно, то оно никогда не применяется к самим открытым данным. Вместо этого открытые данные шифруются любым симметричным шифром со случайным ключом (так называемый «сессионный ключ»), а затем симметричный ключ шифруется асимметричным шифрованием.
  3. Для шифрования используется открытый ключ, а для расшифровки — закрытый. Т.е. кто угодно может зашифровать данные открытым ключом получателя, при этом расшифровать данные сможет только получатель. Так и обеспечивается секретность.

Функции RSAEncrypt/RSADecrypt работают с little-endian данными и используют алгоритм дополнения PKCS#1 Тип 2.

Шифрование файла на диске:

uses
EEncrypt; // для всех RSA функций и SecureFree

procedure TForm1.Button1Click(Sender: TObject);
var
// Случайный симметричный ключ для шифрования данных
SessionKey: TTwofishKey;
SessionKeyRAW: TTwofishRAWKey;
// Асимметричный ключ для шифрования симметричного ключа
RSAKey: TRSAKey;
// Открытые данные для шифрования
Data: TMemoryStream;
// Зашифрованные данные
EncryptedData: TEncryptBuffer;
// Поток для сохранения данных
FS: TFileStream;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(EncryptedData, SizeOf(EncryptedData), 0);

try
// Создадим случайный симметричный ключ
SessionKeyRAW := TwofishInitSessionKeyRAW;
SessionKey := TwofishInitKey(SessionKeyRAW);

// FS используется дважды:
// 1). Для записи зашифрованного симметричного ключа
// 2). Для записи самиз зашифрованных данных

FS := nil;
try

// Шаг 1: шифруем ключ шифрования

// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try

// Шифруем симметричный ключ SessionKeyRAW в буфер EncryptedData
EncryptedData.cbData := SizeOf(SessionKeyRAW);
RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData);

finally
// Как только мы его зашифровали - нам больше не нужен асимметричный ключ
SecureFree(RSAKey);
// Также не нужна и исходная форма ключа
SecureFree(SessionKeyRAW);
end;
try

// Здесь: EncryptedData хранит зашифрованный симметричный ключ

// Сохраним зашифрованный ключ шифрования в сам выходной файл
FS := TFileStream.Create('C:DocumentsEncryptedData.bin', fmCreate or fmShareExclusive);
FS.WriteBuffer(EncryptedData.cbData, SizeOf(EncryptedData.cbData));
FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData);

// Не закрываем FS, мы ещё не записали туда сами данные...

finally
// Удаляем зашифрованный ключ
SecureFree(EncryptedData);
end;

// Далее, у нас есть симметричный ключ шифрования SessionKey и открытый выходной файл FS


// Шаг 2: шифруем данные

Data := TMemoryStream.Create;
try

// Загрузим открытый текст для шифрования
Data.LoadFromFile('C:DocumentsText.txt');

// Шифруем файл
// Поскольку ключ шифрования - случайный, то вектор инициализации не используется
// Поэтому шифрование происходит в режиме ECB
EncryptedData.cbData := Cardinal(Data.Size);
TwofishEncrypt(SessionKey, Data.Memory, Pointer(EncryptedData.pbData), EncryptedData.cbData);

// Ключ больше не нужен
SecureFree(SessionKey);

// Сохраняем зашифрованные данные в файл
FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData);

finally
// Удаляем данные
SecureFree(Data);
SecureFree(EncryptedData);
end;

finally
// Закрываем файл
FreeAndNil(FS);
end;

// Теперь файл C:DocumentsEncryptedData.bin полностью готов
// В нём лежит зашифрованный (случайный) симметричный ключ, а также зашифрованный файл C:DocumentsText.txt
// Его сможет расшифровать только тот, у кого есть закрытый асимметричный ключ, соответствующий использованному открытому асимметричному ключу
// Т.е. файл можно передать получателю любым способом, в том числе - по открытым каналам связи

finally
// Чистим всё
SecureFree(SessionKey);
end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
// Случайный симметричный ключ для шифрования данных
SessionKey: TTwofishKey;
// Асимметричный ключ для шифрования симметричного ключа
RSAKey: TRSAKey;
// Открытые данные для шифрования
Data: TMemoryStream;
// Расшифрованные данные
DecryptedData: TEncryptBuffer;
// Поток для сохранения данных
FS: TFileStream;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(DecryptedData, SizeOf(DecryptedData), 0);

// Открываем зашифрованный файл
FS := TFileStream.Create('C:DocumentsEncryptedData.bin', fmOpenRead or fmShareDenyWrite);
try

try
// Шаг 1: расшифровываем сессионный ключ

// Сначала читаем из файла зашифрованный (случайный) симметричный ключ
FS.ReadBuffer(DecryptedData.cbData, SizeOf(DecryptedData.cbData));
DecryptedData.pbData := AllocMem(DecryptedData.cbData);
try
FS.ReadBuffer(DecryptedData.pbData^, DecryptedData.cbData);

// Расшифровываем симметричный ключ с помощью закрытого асимметричного ключа
RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
try
EEncrypt.RSADecrypt(RSAKey, DecryptedData);
finally
SecureFree(RSAKey);
end;

// Инициализируем симметричный ключ
Assert(DecryptedData.cbData = SizeOf(TTwofishRAWKey));
SessionKey := TwofishInitKey(TTwofishRAWKey(Pointer(DecryptedData.pbData)^));

finally
// Удаляем ненужные данные
SecureFree(DecryptedData);
end;

// Теперь у нас есть симметричный ключ SessionKey, которым мы можем расшифровать файл

// Шаг 2: расшифровываем сами данные

Data := TMemoryStream.Create;
try
// Остаток в файле - это сам зашифрованный файл
Data.CopyFrom(FS, FS.Size - FS.Position);

// Расшифровываем файл
DecryptedData.cbData := Cardinal(Data.Size);
TwofishDecrypt(SessionKey, Data.Memory, Pointer(DecryptedData.pbData), DecryptedData.cbData);

// Ключ больше не нужен
SecureFree(SessionKey);

// Сохраняем расшифрованный результат в файл
FreeAndNil(FS);
FS := TFileStream.Create('C:DocumentsText2.txt', fmCreate or fmShareExclusive);
try
FS.WriteBuffer(DecryptedData.pbData^, DecryptedData.cbData);
finally
FreeAndNil(FS);
end;

// Данные больше не нужны
SecureFree(DecryptedData);

// Здесь: Text2.txt должен стать точной копией Text.txt

finally
SecureFree(Data);
end;

finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(SessionKey);
SecureFree(DecryptedData);
end;
finally
FreeAndNil(FS);
end;
end;

Цифровая подпись

Когда для шифрования используется закрытый ключ, а для расшифровки используется открытый — это называется «цифровая подпись». Поскольку расшифровать данные может любой (т.к. открытый ключ есть у всех), то секретность данных таким способом не обеспечивается. Но зато, если данные удаётся расшифровать чьим-то открытым ключом — мы можем быть уверены, что данные были зашифрованны именно им (ибо секретный закрытый ключ есть только у этого лица). Таким образом, мы можем проверить аутентичность данных.

При этом как правило сами данные не шифруются. Вместо этого от данных вычисляют хэш и его шифруют. Для подписи произвольных данных и её проверки в EurekaLog есть функции RSASign (использует закрытый ключ) и RSAVerify (использует открытый ключ). Функции цифровой подписи EurekaLog используют SHA1 со схемой EMSA-PKCS1 для дополнения данных.

Результирующая цифровая подпись представляет собой непрозрачный набор байт произвольной длины. Если вы захотите обмениваться цифровой подписью с другими окружениями — нужно помнить, что Windows/Delphi используют little endian порядок байт, в то время как некоторые другие окружения (например, .NET или PHP) используют big endian. Поэтому в некоторых случаях порядок байт цифровой подписи нужно обратить.

Например:

Запрос лицензии от PHP-скрипта:

uses
EEncrypt, // для всех RSA функций и SecureFree
EEncoding, // для Base64
EJSON, // для JSON функций
EWebTools; // для интернет-функций

procedure TForm1.Button1Click(Sender: TObject);
var
// Данные для отправки PHP-скрипту
JSON, JSONRequest, JSONUser: IJSONValues;
JSONText: String;
JSONRAW: RawByteString;
// Случайный симметричный ключ для шифрования данных (сессионный ключ)
SessionKey: TTwofishKey;
SessionKeyRAW: TTwofishRAWKey;
// Асимметричный ключ для шифрования сессионного ключа
RSAKey: TRSAKey;
// Зашифрованные данные
EncryptedData: TEncryptBuffer;
// Текстовое представление зашифрованных данных
EncodedKey, EncodedData: String;
// URL для вызова PHP
URL: String;
// Ответ от PHP (байты)
ReplyRAW: RawByteString;
// Текстовое представление лицензии
EncodedLicense: String;
// Текстовое представление цифровой подписи
EncodedSignature: String;
// Лицензия (байты)
License: RawByteString;
// Цифровая подпись (байты)
Signature: RawByteString;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(EncryptedData, SizeOf(EncryptedData), 0);

try

// Шаг 1: готовим данные для отправки PHP-скрипту
JSON := JSONCreate;
JSONRequest := JSONCreate;
JSONUser := JSONCreate;

JSONRequest['version'] := 1;
JSONRequest['type'] := 'license';
JSONRequest['scope'] := 'installer';

JSONUser['login'] := 'input-from-edit1';
JSONUser['password'] := 'input-from-edit2';

JSON['app'] := 'MyApp';
JSON['version'] := GetModuleVersion(GetModuleName(HInstance));
JSON['date'] := Now;
JSON['request'] := JSONRequest;
JSON['user'] := JSONUser;

JSONText := JSON.ToString;
Finalize(JSONUser); // не обязательно
Finalize(JSONRequest); // не обязательно
Finalize(JSON); // не обязательно
(*

Здесь JSONText содержит:

{
"app": "MyApp",
"version": "1.0.0.0",
"date": "2021.06.25 14:04:21",
"request": {
"version": 1,
"type": "license",
"scope": "installer"
}
"user": {
"login": "input-from-edit1",
"password": "input-from-edit2"
}
}

*)
JSONRAW := UTF8Encode(JSONText);
SecureFree(JSONText);

// Здесь JSONRAW содержит байты для отправки PHP-скрипту


// Шаг 2: шифруем данные запроса и отправляем PHP-скрипту

// Создадим случайный симметричный ключ
SessionKeyRAW := TwofishInitSessionKeyRAW;
SessionKey := TwofishInitKey(SessionKeyRAW);

// Шаг 2а: шифруем ключ шифрования

// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try

// Шифруем симметричный ключ SessionKeyRAW в буфер EncryptedData
EncryptedData.cbData := SizeOf(SessionKeyRAW);
RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData);
try

// Как только мы его зашифровали - нам больше не нужен асимметричный ключ
SecureFree(RSAKey);
// Также не нужна и исходная форма ключа
SecureFree(SessionKeyRAW);

// Кодируем зашифрованные данные в текст
EncodedKey := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData);

// Удаляем ненужный данные
SecureFree(EncryptedData);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(EncryptedData);
end;

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RSAKey);
SecureFree(SessionKeyRAW);
end;

// Здесь: EncodedKey хранит зашифрованный симметричный ключ (в текстовом виде)
// Далее, у нас есть симметричный ключ шифрования SessionKey и данные для отправки JSONRAW

// Шаг 2б: шифруем данные запроса

// Поскольку ключ шифрования - случайный, то вектор инициализации не используется
// Поэтому шифрование происходит в режиме ECB
EncryptedData.cbData := Length(JSONRAW);
TwofishEncrypt(SessionKey, Pointer(JSONRAW), Pointer(EncryptedData.pbData), EncryptedData.cbData);

try
// Ключ больше не нужен
SecureFree(SessionKey);

// Кодируем зашифрованные данные в текст
EncodedData := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData);

// Удаляем ненужные данные
SecureFree(EncryptedData);

// Отправляем зашифрованные данные и ключ скрипту
// Здесь URLEncode нужно для экранирования '+'
URL := 'http://localhost/test.php?key=' + URLEncode(EncodedKey) + '&data=' + URLEncode(EncodedData);
if not InitWebTools then
RaiseLastOSError;
try
ReplyRAW := InternetGet(URL, [], []);
finally
DoneWebTools;
end;

// PHP-скрипт ничего не вернул?
if ReplyRAW = '' then
Abort;

// Опционально
SecureFree(URL);

// Удаляем ненужные данные
SecureFree(EncodedKey);
SecureFree(EncodedData);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(SessionKey);
SecureFree(EncryptedData);
SecureFree(EncodedKey);
SecureFree(EncodedData);
end;

finally
// На всякий случай (исключение) - чистим данные
SecureFree(SessionKey);
SecureFree(SessionKeyRAW);
SecureFree(EncodedData);
SecureFree(EncodedKey);
SecureFree(JSONText);
SecureFree(JSONRAW);
end;

// Здесь у нас есть ReplyRAW - ответ от PHP-скрипта (байты)

try
// Конвертируем байты в текст
JSONText := UTF8ToString(ReplyRAW);

// Опционально
Finalize(ReplyRAW);

// Конвертируем текст JSON в объект JSON
JSON := JSONCreate(JSONText);

// Опционально
Finalize(JSONText);

// PHP-скрипт вернул ошибку?
if JSON.IndexOf('error') >= 0 then
raise Exception.Create(JSON['error']);

// Читаем лицензию и подпись
EncodedLicense := JSON['license'];
EncodedSignature := JSON['signature'];

// Текст в байты
License := Base64DecodeString(EncodedLicense);
Signature := Base64DecodeString(EncodedSignature);

// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try

// Проверяем цифровую подпись
if RSAVerify(RSAKey, Pointer(License), Length(License), Pointer(Signature), Length(Signature)) then
begin
// Цифровая подпись не нарушена
// Мы можем быть уверены, что License действительно пришла с нашего сервера

// Опционально
SecureFree(RSAKey);

// Просто для примера
// В реальной программе License была бы зашифрована
ShowMessage(UTF8ToString(License));
// Покажет 'This is just an example license'

end
else
ShowMessage('Not signed');

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RSAKey);
end;

finally
// На всякий случай (исключение) - чистим данные
SecureFree(JSONText);
SecureFree(ReplyRAW);
SecureFree(EncodedSignature);
SecureFree(EncodedLicense);
SecureFree(Signature);
SecureFree(License);
end;
end;
<?php

// Эти функции необходимы, потому что MCrypt использует дополнение нулями вместо PKCS#5
// OpenSSL поддерживает PKCS#5, но не поддерживает Twofish

// Дополнение данных по PKCS#5
function pkcs5_pad($text, $blocksize = 16) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}

// Обрезка PKCS#5
function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}

// Секретный ключ
// Должен соответствовать открытому ключу, используемому в Delphi
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
$PrivateKey = <<<EOD
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u
...
X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK
N9AYlDckmF/X/1cZxWNyiltZZ542kezn
-----END PRIVATE KEY-----
EOD;

// Читаем переданные данные (зашифрованный сессионный ключ и зашифрованные данные)
$EncodedKey = $_GET['key'];
$EncodedData = $_GET['data'];

// Декодируем текст в байты
$EncryptedKey = base64_decode($EncodedKey);
$EncryptedData = base64_decode($EncodedData);

// Переводим little-endian (Windows/EurekaLog/WinCrypt) в big-endian (PHP/OpenSSL)
$EncryptedKey = strrev($EncryptedKey);

// Расшифровываем сессионный ключ
if (!openssl_private_decrypt($EncryptedKey, $Key, $PrivateKey)) {
echo('{ "error": ' . json_encode('openssl_private_decrypt: ' . openssl_error_string()) . ' }');
die();
}

// Расшифровываем текст (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$Data = pkcs5_unpad(mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedData, MCRYPT_MODE_ECB));

// Преобразуем строку JSON в объект JSON
$Data = json_decode($Data, true);

// Проверяем тип запроса
$Request = $Data['request'];
if (($Request['version'] < 1) || ($Request['version'] > 1)) {
echo('{ "error": "Unsupported request" }');
die();
}

// Запрашивают лицензию?
if ($Request['type'] == 'license') {

// Проверяем пользователя
$User = $Data['user'];
// Только для примера
$OK = (($User['login'] == 'input-from-edit1') && ($User['password'] == 'input-from-edit2'));
if ($OK) {

// Каким-то образом получаем лицензию для указанного пользователя
// В реальной программе это будут, вероятно, зашифрованные данные
$License = 'This is just an example license';

// Подписываем лицензию
openssl_sign($License, $Signature, $PrivateKey, OPENSSL_ALGO_SHA1);

// Переводим big-endian (PHP/OpenSSL) в little-endian (Windows/EurekaLog/WinCrypt)
$Signature = strrev($Signature);

// Кодируем байты в текст
$EncodedLicense = base64_encode($License);
$EncodedSignature = base64_encode($Signature);

// Возвращаем вызывающему лицензию с подписью
echo('{ "license": ' . json_encode($EncodedLicense) . ', "signature": ' . json_encode($EncodedSignature) . ' }');

} else {
echo('{ "error": "Access Denied" }');
die();
}
}

echo('{ "error": "Unsupported request" }');

Читать на сайте автора.

Работа с криптографией в EurekaLog

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

EurekaLog предлагает три модуля:

  1. EEncoding — содержит функции кодирования и преобразования данных;
  2. EHash — содержит функции хэширования;
  3. EEncrypt — содержит функции симметричного и ассиметричного шифрования.

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

Важное примечание: обновите EurekaLog до самой последней версии. Не все описываемые тут возможности доступны в предыдущих версиях, там некоторые функции были опубликованы специально для статьи.

Кодирование

Прежде, чем говорить о криптографии, нужно определиться с исходными данными. Например, пусть вы хотите получить MD5-хэш строки 'Привет'. Но что вы подадите на вход функции хэша? Байты $CF$F0$E8$E2$E5$F2 ('Привет' в ANSI/Windows-1251)? Байты $1F$04$40$04$38$04$32$04$35$04$42$04 ('Привет' в Unicode/UTF-16)? Байты $D0$9F$D1$80$D0$B8 ('Привет' в UTF-8)? В зависимости от того, как вы определите этот вопрос, вы получите разный результат. Например, MD5-хэш для строки 'Привет' в UTF-16 будет 8EFA2364EE560EE1B862ECC8D430C9AD, для 'Привет' в ANSI — это 43A3F987A7AF93811B7682E43ED0752A, а для 'Привет' в UTF-8 — 8A669E9418750C81AB90AE159A8EC410.

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

Поэтому, когда вам нужна точная определённость в результате, вы должны оперировать байтами, а не строками. В Delphi для оперирования с байтами вы можете:

  • Использовать указатель на данные + их размер: (const ABuffer: Pointer; const ABufferSize: Cardinal);
  • Использовать TBytes (array of Byte — динамический массив байтов);
  • Использовать RawByteString;
  • Использовать TStream и его наследников.

В частности, в EurekaLog функции принимают пару параметров указатель+размер, а также имеются перегруженные варианты, принимающие RawByteString.

К примеру, если вы попробуете вычислить MD5-хэш «просто» строки 'Привет' в PHP, вы получите 8a669e9418750c81ab90ae159a8ec410 — т.е. MD5-хэш от UTF-8 представления 'Привет'.

Откуда также можно сделать вывод, что строки в PHP хранятся в UTF-8; для сравнения: в Delphi строки хранятся как UTF-16 (начиная с Delphi 2009), либо в ANSI (Delphi 2007 и ранее).

Если вы захотите изменить кодировку в PHP — вам понадобится вызвать что-то типа mb_convert_encoding. А если вы захотите изменить кодировку в Delphi — вам понадобятся функции Delphi. В частности, для конвертации в/из UTF-8, TEncoding. В Delphi 2009 и выше вы также можете просто объявить строковый тип нужной кодировки и конвертирование данных строки будет выполнено автоматически при присвоении.

Сказанное верно и в обратную сторону: результатом вызова криптографических функций является набор байт (хэш, зашифрованные данные, и т.д.). Если вы захотите отобразить этот результат человеку — вам придётся конвертировать его в строку. Делать это можно, опять же, разными способами. К примеру, вы можете использовать встроенную функцию BinToHex или её более удобные аналоги HexEncodeString/HexEncodeToString из EurekaLog. Можно использовать Base64EncodeString/Base64EncodeToString из EurekaLog. Если, вдруг, вам нужно преобразовать данные из/в RawByteString, то в EurekaLog есть хэлперы RAWToString/RAWFromString. Также для небольших по размеру данных вы вполне можете захотеть загружать/сохранять их в файлы — для этого есть FileToString/StringToFile (из модуля ECompatibility).

Примеры использования упомянутых функций можно посмотреть ниже.

Хэширование

В EurekaLog есть функции для вычисления таких хэшей:

  • CRC16
  • CRC32
  • MD5
  • SHA-1
  • SHA-256
  • SDBM — это неплохая хэш-функция общего назначения с равномерным распределением, удобна для использования в качестве ключа/индекса в БД

Все функции хэширования имеют имя ИмяХэшаHash (например, MD5Hash()), возвращают результат типа TИмяХэшаHash (например, TSHA1Hash), а на вход принимают RawByteString, либо пару указатель+данные.

Также для некоторых хэшей в EurekaLog есть реализация алгоритма HMAC, который может использоваться для различных целей. Один из способов использования — аутентификация пользователя путём комбинации соли (salt) и пароля для получения хэша через HMAC. Функции для HMAC имеют вид ИмяХэшаHMAC (например, MD5HMAC()) и принимают пароль и соль.

Вот несколько примеров:

1. Считаем хэш строки:

uses
EEncoding, // для HexEncodeToString
EHash; // для MD5Hash

procedure TForm1.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
UTF8Str: UTF8String; // Байтовое представление
Hash: TMD5Hash; // Хэш
begin
// Определим исходные данные
S := 'Привет';

// Определим точное представление в байтах
// В данном случае - UTF-8
UTF8Str := UTF8Encode(S);
// (можно просто UTF8Str := S; в Delphi 2009 и выше)

// Вычислим хэш
Hash := MD5Hash(UTF8Str);

// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет '8A669E9418750C81AB90AE159A8EC410'
end;

2. Считаем хэш файла:

uses
EEncoding, // для HexEncodeToString
EHash, // для SHA256Hash
ECompatibility; // для FileToString

procedure TForm1.Button1Click(Sender: TObject);
var
Content: RawByteString; // Исходные байты файла
Hash: TSHA256Hash; // Хэш
begin
// Загружаем файл в двоичную строку
Content := FileToString(ParamStr(0));
// В Content будет 'MZP'#0#2#0#0#0...

// Вычислим хэш
Hash := SHA256Hash(Content);
Finalize(Content); // не обязательно

// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет что-то вроде 'FCF52FDC753E3797FE5EE4B5A7680E656D044D6BF7D97C408F0F7874492E43C2'
end;

3. Считаем хэш строки в произвольной кодировке:

uses
EEncoding, // для HexEncodeToString (и TEncoding для старых Delphi)
EHash; // для CRC32Hash

procedure TForm21.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
Encoding: TEncoding; // Кодировка для конвертирования в байты
Content: TBytes; // Байтовое представление
Hash: TCRC32Hash; // Хэш
begin
// Определим исходные данные
S := 'Привет';

// Получаем любую кодировку
Encoding := TEncoding.GetEncoding(866);
// Также можно:
// Encoding := TEncoding.UTF8;
// Encoding := TEncoding.Unicode;
// Encoding := TEncoding.ANSI;
try

// Конвертируем строку в байты
Content := Encoding.GetBytes(S);

finally
FreeAndNil(Encoding);
end;

// Вычислим хэш
Hash := CRC32Hash(Pointer(Content), Length(Content));
Finalize(Content); // не обязательно

// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет '6DB3A7B9'
// Также можно IntToStr(Hash) - что даст 3114775405
end;

4. Обмениваемся хэшем с PHP:

uses
EEncoding, // для HexEncodeToString
EHash, // для MD5Hash
ECore; // для ShellExec

procedure TForm1.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
UTF8Str: UTF8String; // Байтовое представление строки
Hash: TMD5Hash; // Хэш
HashStr: String; // Текстовое представление хэша
begin
// Определим исходные данные
S := 'Привет';

// Определим точное представление в байтах
// В данном случае - UTF-8
UTF8Str := UTF8Encode(S);
// (можно просто UTF8Str := S; в Delphi 2009 и выше)

// Вычислим хэш
Hash := MD5Hash(UTF8Str);

// Преобразуем двоичное представление в текст
HashStr := HexEncodeToString(@Hash, SizeOf(Hash));
// Хранит '8A669E9418750C81AB90AE159A8EC410'

// Передадим хэш в скрипт PHP
ShellExec(Format('http://localhost/test.php?hash=%s', [HashStr]));
end;
<?php

// Исходная строка (хранится в UTF-8)
$Source = 'Привет';

// Вычислим хэш исходной строки
// (функция вернёт строковое представление хэша)
$Hash = md5($Source);

// Прочитаем аргумент скрипта
$HashArg = $_GET['hash'];

// Проверим, совпадает ли хэш, сравнивая строковые представления
if (strtolower($Hash) == strtolower($HashArg)) {
// также можно использовать (начиная с PHP 5.6):
// if (hash_equals($HashArg, $Hash)) {

echo('OK'); // должны попасть сюда,
// т.е. исходные строки совпадают
// 'Привет' в Delphi = 'Привет' в PHP
} else {
echo('FAIL');
}

5. Хранение учётных данных в БД:

uses
EHash, // для SHA256HMAC
EEncrypt, // для InitSalt
EEncoding; // для RAWToString и HexEncodeToString/HexDecodeFromString

procedure TForm1.Button1Click(Sender: TObject);
var
UserName: String; // Пользовательский логин
UserPassword: String; // Пользовательский пароль
UserPasswordRAW: RawByteString; // Двоичное представление пароля
Salt: TSalt; // Соль (salt)
SaltStr: String; // Строковое представление соли
Hash: TSHA256Hash; // Хэш пароля
Hash2: TSHA256Hash; // Сохранённый хэш пароля
HashStr: String; // Строковое представление хэша
begin
// Шаг 1. Регистрация аккаунта

// Каким-то образом получаем логин и пароль
UserName := InputBox('Sign in', 'Enter the login:', '');
UserPassword := InputBox('Sign in', 'Enter the password:', '');

// Генерируем случайные байты для использования их в качестве соли
Salt := InitSalt;

// Преобразуем пароль в двоичное представление
UserPasswordRAW := UTF8Encode(UserPassword);

// Вычисляем хэш от соли и пароля через HMAC
Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW));

// Кодируем двоичные данные в текст
SaltStr := HexEncodeToString(@Salt, SizeOf(Salt));
HashStr := HexEncodeToString(@Hash, SizeOf(Hash));

// Вставляем в БД новую запись
// Это псевдо-код
InsertIntoDBTable('users', ['login', 'salt', 'password'], [UserName, SaltStr, HashStr]);
// Здесь:
// 'users' - имя таблицы
// 'login' - строковое поле произвольной длины
// 'salt' - строковое поле в 32 символа или двоичное поле в 16 байт
// 'password' - строковое поле в 64 символа или двоичное поле в 32 байта



// Шаг 2. Проверка логина

// Каким-то образом получаем логин и пароль
UserName := InputBox('Log in', 'Enter the login:', '');
UserPassword := InputBox('Log in', 'Enter the password:', '');

// Ищем в БД запись пользователя с введённым логином
// Это псевдо код, просто для примера
// В реальном коде нужно использовать параметры БД
Query := Format('SELECT salt, password FROM users WHERE login = ''%s'' LIMIT 1', [UserName]);
Values := DBQuery(Query);

// Если записи нет - введён неверный логин
if Length(Values) = 0 then
begin
ShowMessage('Invalid login');
Exit;
end;

// Преобразуем соль и хэш из текста в двоичные данные
SaltStr := Values[0]; // поле 'salt' из SELECT
HashStr := Values[1]; // поле 'password' из SELECT

Assert(HexCalcDecodedSize(Length(SaltStr)) = SizeOf(Salt));
HexDecodeFromString(SaltStr, @Salt);

Assert(HexCalcDecodedSize(Length(HashStr)) = SizeOf(Hash2));
HexDecodeFromString(HashStr, @Hash2);

// Повторим вычисления хэша, как и выше
UserPasswordRAW := UTF8Encode(UserPassword);
Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW));

// Здесь у нас есть:
// Hash - хэш введённого пароля
// Hash2 - хэш сохранённого пароля
// Если два хэша равны, то равны и пароли

// Проверяем, что пароль был введён верно, сравнивая хэши
if CompareMem(@Hash, @Hash2, SizeOf(Hash)) then
ShowMessage('OK')
else
ShowMessage('Invalid password');
end;
<?php

// Шаг 1. Регистрация аккаунта

// Каким-то образом получаем логин и пароль
$UserName = $_GET['login'];
$UserPassword = $_GET['password'];

// Генерируем случайные байты для использования их в качестве соли
$Salt = random_bytes(16);

// Вычисляем хэш от соли и пароля через HMAC
$HashStr = hash_hmac('sha256', $Salt, $UserPassword);

// Кодируем двоичные данные в текст
$SaltStr = bin2hex($Salt);

// Вставляем в БД новую запись
// Это псевдо-код
InsertIntoDBTable('users', ['login', 'salt', 'password'], [$UserName, $SaltStr, $HashStr]);
// Здесь:
// 'users' - имя таблицы
// 'login' - строковое поле произвольной длины
// 'salt' - строковое поле в 32 символа или двоичное поле в 16 байт
// 'password' - строковое поле в 64 символа или двоичное поле в 32 байта



// Шаг 2. Проверка логина

// Каким-то образом получаем логин и пароль
$UserName = $_GET['login'];
$UserPassword = $_GET['password'];

// Ищем в БД запись пользователя с введённым логином
// Это псевдо код, просто для примера
// В реальном коде нужно использовать параметры БД
$Query = 'SELECT salt, password FROM users WHERE login = '' . $UserName . '' LIMIT 1';
$Values = DBQuery($Query);

// Если записи нет - введён неверный логин
if (empty($Values)) {
echo('Invalid login');
die;
}

// Преобразуем соль и хэш из текста в двоичные данные
$SaltStr = Values['salt']; // поле 'salt' из SELECT
$HashStr2 = Values['password']; // поле 'password' из SELECT

$Salt = hex2bin($SaltStr);

// Повторим вычисления хэша, как и выше
$HashStr = hash_hmac('sha256', $Salt, $UserPassword);

// Здесь у нас есть:
// $HashStr - строковое представление хэша введённого пароля
// $HashStr2 - строковое представление хэша сохранённого пароля
// Если два хэша равны, то равны и пароли

// Проверяем, что пароль был введён верно, сравнивая хэши
if (strtolower($HashStr) == strtolower($HashStr2)) {
// также можно использовать (начиная с PHP 5.6):
// if (hash_equals($HashStr2, $HashStr)) {

echo('OK'); // должны попасть сюда,
// т.е. пароли совпали
} else {
echo('Invalid password');
}

Шифрование

В EurekaLog есть такие функции для шифрования:

  • Внутрипроцессное шифрование (например, для защиты паролей)
  • Межпроцессное шифрование (для защиты внешних данных в рамках пользовательского аккаунта или всего компьютера)
  • TEA
  • Twofish
  • RSA

Как и функции хэширования, функции шифрования принимают на вход указатель на данные + их размер, либо RawByteString. Но поскольку, в отличие от функций хэширования, функциям шифрования также нужно возвращать данные произвольного размера — вы также можете использовать запись TEncryptBuffer, которая просто объединяет указатель + размер в один параметр.

Внутрипроцессное шифрование

Иногда в приложении необходимо оперировать «секретной» информацией. Чтобы уменьшить риски утечки этой информации, необходимо хранить её в зашифрованном виде. Подробнее о такой практике можно почитать в MSDN или в (крайне рекомендую) книге. EurekaLog предоставляет функции для оперирования конфиденциальной информацией:

Функция MemProtect зашифровывает указанный блок памяти в процессе так, что расшифровать его можно только из этого же процесса. Функция MemUnprotect расшифровывает блок памяти, зашифрованный MemProtect. Функцию же SecureFree можно использовать для удаления почти чего угодно. Функция затрёт содержимое перед освобождением памяти.

Например:

uses
EEncrypt; // для MemProtect/MemUnprotect и SecureFree

procedure TForm1.Button1Click(Sender: TObject);
var
UserPassword: String;
StoredPassword: TEncryptBuffer;
ClearText: TEncryptBuffer;
begin
// Обнуляем все буферы
FillChar(StoredPassword, SizeOf(StoredPassword), 0);
FillChar(ClearText, SizeOf(ClearText), 0);

// Каким-то образом получаем конфиденциальную информацию
UserPassword := InputBox('Query', 'Enter the password:', '');
try

// Шифруем информацию
ClearText.pbData := Pointer(UserPassword);
ClearText.cbData := Length(UserPassword) * SizeOf(Char);
MemProtect(ClearText, StoredPassword);

finally
// Затираем оригинал в открытом виде
SecureFree(UserPassword);
// Нет необходимости удалять ClearText,
// поскольку мы не выделяли для него память
end;

// ...

// Далее у нас есть StoredPassword - информация в зашифрованном виде
// Каждый раз, когда нам надо использовать конфиденциальную информацию,
// мы должны расшифровать её, использовать, затем удалить расшифрованный вариант

// ...

// Расшифровываем информацию
MemUnprotect(StoredPassword, ClearText);
try

// Как-то используем конфиденциальную информацию
Hash := MD5Hash(ClearText.pbData, ClearText.cbData);

finally
// Затираем оригинал в открытом виде
SecureFree(ClearText);

// Также можно удалить производную информацию
SecureFree(Hash);
end;

// ...

// В конце работы - удаляем зашифрованную информацию
SecureFree(StoredPassword);
end;

Межпроцессное шифрование

Иногда возникает необходимость хранить конфиденциальную информацию. Например, опция «Запомнить меня» может сохранить пароль аккаунта в реестре. В этом случае функции MemProtect/MemUnprotect не помогут, поскольку они не работают между процессами (а перезапуск программы и чтение сохранённых данных — это новый процесс). Для таких случаев в EurekaLog есть похожие функции DataProtect и DataUnprotect. Например:

uses
EEncrypt, // для DataProtect/DataUnprotect и SecureFree
EConfig, // для RegKeyWrite/RegKeyRead
EEncoding; // для Base64EncodeString/Base64DecodeString

procedure TForm1.Button1Click(Sender: TObject);
var
UserPassword: String;
StoredPassword: RawByteString;
ClearText: RawByteString;
begin
// Каким-то образом получаем конфиденциальную информацию
UserPassword := InputBox('Query', 'Enter the password:', '');
try

// Конвертируем в RawByteString для удобства
ClearText := UTF8Encode(UserPassword);

// Затираем оригинал в открытом виде
SecureFree(UserPassword);

// Шифруем информацию
StoredPassword := DataProtect(ClearText);
// или:
// StoredPassword := DataProtect(ClearText, True);
// если вы хотите использовать HKEY_LOCAL_MACHINE ниже

// Затираем оригинал в открытом виде
SecureFree(ClearText);

// Кодируем зашифрованные двоичные данные в строку (текст)
UserPassword := Base64EncodeString(StoredPassword);

// Сохраняем зашифрованную конфиденциальную информацию в реестре
RegKeyWrite(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'SavedPassword', UserPassword);

// Опционально
SecureFree(UserPassword);

finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(UserPassword);
SecureFree(StoredPassword);
SecureFree(ClearText);
end;

// ...

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

// ...

// Читаем сохранённую зашифрованную конфиденциальную информацию
UserPassword := RegKeyRead(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'SavedPassword', '');
try

// Декодируем строку (текст) в зашифрованные двоичные данные
StoredPassword := Base64DecodeString(UserPassword);

// Опционально - удаляем прочитанные зашифрованные данные
SecureFree(UserPassword);

// Расшифровываем информацию
ClearText := MemUnprotect(StoredPassword);

// Опционально - удаляем прочитанные зашифрованные данные
SecureFree(StoredPassword);

// Поскольку ранее для удобства мы кодировали данные - нужно сделать обратную конвертацию
UserPassword := UTF8ToString(ClearText);

// Затираем оригинал в открытом виде
SecureFree(ClearText);

// Как-то используем конфиденциальную информацию
Hash := MD5Hash(Pointer(UserPassword), Length(UserPassword) * SizeOf(Char));

// Затираем оригинал в открытом виде
SecureFree(UserPassword);

finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(UserPassword);
SecureFree(StoredPassword);
SecureFree(ClearText);
end;
end;

Симметричное шифрование

Для симметричного шифрования EurekaLog поддерживает TEA и Twofish. Оба алгоритма шифрования не патентованы и могут быть свободно использованы в любой программе. TEA используется в широком спектре аппаратного обеспечения благодаря крайне низким требованиям к памяти и простоте реализации. Twofish — надёжный алгоритм симметричного шифрования общего назначения.

Обратите внимание, что зашифрованные данные могут быть больше по размеру чем исходные — поскольку симметричные алгоритмы шифрования часто оперируют блоками данных. Отличительной особенностью TEA является то, что зашифрованные данные будут равны по размеру исходным данным, поэтому у функций шифрования/расшифровки TEA есть перегруженный вариант для операций in-place, т.е. без перераспределения памяти. Для алгоритма Twofish размер данных должен быть кратен размеру блока (16 байт) — в противном случае данные будут дополнены (padding) алгоритмом PKCS#5 до минимально необходимого размера.

Для обоих алгоритмов EurekaLog предоставляет algEncrypt/algDecrypt функции, которые выглядят и работают одинаково. На вход функции принимают ключ шифрования и исходные данные. Разница есть только в том, что варианты для Twofish позволяют указать необязательный вектор инициализации (IV — initialization vector). Вектор инициализации — это просто случайный набор байт (который можно получить вызовом функции TwofishInitIV), который служит для того, чтобы два одинаковых блока данных шифровались бы по-разному, некий аналог соли (salt). Вектор инициализации не должен быть секретным и он передаётся (если есть) вместе с зашифрованными данными.

Оба алгоритма используют двоичные ключи для шифрования и расшифровки. Поскольку алгоритмы симметричные, то ключ для шифрования обязан совпадать с ключём для дешифрования. Разумеется, ключи для TEA и Twofish имеют разный (но фиксированный) размер в байтах. Как правило, ключи шифрования — не случайны, а получаются из паролей, вводимых пользователем. Чтобы преобразовать произвольный пароль в ключ фиксированной длины — используются функции algDeriveKey, на вход которым передаётся блок данных произвольного размера. «Вывод» ключа из пароля получается простым вызовом функции хэша от пароля с подходящим по размеру результатом. Например, для TEA это будет MD5, для Twofish — SHA-256. У derive-функций также есть перегруженный вариант, принимающий пароль в виде строки с опциональной солью (salt). В этом случае пароль конвертируется в UTF-8 представление, и хэш берётся от строки ‘соль’ + ‘UTF-8 пароль’. Кроме того, есть ещё вариант функции algDeriveKeyHMAC, который использует алгоритм HMAC для комбинации соли и пароля. В целом, если вы планируете использовать соль, то мы рекомендуем использовать функции algDeriveKeyHMAC.

Если ключи шифрования получаются как-то иначе (не из пароля) — вы также можете обмениваться ими напрямую, не «выводя» их из пароля. Просто обращайтесь с ключём, как с записью/набором байт фиксированного размера. Единственная тонкость — EurekaLog использует оптимизацию с Twofish: ключ используется не напрямую, а сначала преобразовывается в промежуточный вариант, что позволяет оптимизировать операции шифрования и расшифровки. Исходный ключ называется TTwofishRAWKey, а оптимизированный вариант — TTwofishKey.

Например:

1. Шифрование и расшифровка данных по паролю:

uses
EEncrypt; // для InitSalt, TEADeriveKey, TEAEncrypt/TEADecrypt, SecureFree

procedure TForm1.Button1Click(Sender: TObject);
var
Salt: TSalt; // Соль для преобразования пароля в ключ
Key: TTEAKey; // Ключ для шифрования и дешифрования
Source: String; // Исходный текст
SourceBytes: RawByteString; // Байтовое представление исходного текста
EncryptedBytes: RawByteString; // Зашифрованные исходные данные
begin
// Обнуляем все буферы
FillChar(Salt, SizeOf(Salt), 0);
FillChar(Key, SizeOf(Key), 0);

// Шаг 1: шифрование
try

// Определили исходные данные
Source := 'Привет';

// Определили байтовое представление исходных данных
// В данном случае - UTF-8
SourceBytes := UTF8Encode(Source);

// Исходные данные больше не нужны - затираем
SecureFree(Source);

// Создали случайный набор байтов для соли
Salt := InitSalt;

// Получили ключ из пароля
Key := TEADeriveKeyHMAC('супер секретный пароль', Salt);

// Зашифровали открытые данные
EncryptedBytes := TEAEncrypt(Key, SourceBytes);

// Исходные данные и ключ больше не нужны - затираем
SecureFree(Key);
SecureFree(SourceBytes);

finally
// На всякий случай (исключение) - затёрли и освободили всё
SecureFree(Source);
SecureFree(SourceBytes);
SecureFree(Key);
end;

// Далее у нас есть:
// Salt - соль для конвертации пароля в ключ
// EncryptedBytes - зашифрованные данные произвольного размера
// Именно эти данные нужно передать на сторону для расшифровки

// Шаг 2: расшифровка
try
// Получили ключ из пароля (Salt нам должны передать)
Key := TEADeriveKeyHMAC('супер секретный пароль', Salt);
// Здесь Key должен совпасть с ключом,
// который мы использовали для шифрования

// Расшифровали данные
SourceBytes := TEADecrypt(Key, EncryptedBytes);

// Удалили ключ
SecureFree(Key);

// Опционально
SecureFree(EncryptedBytes);

// Декодировали исходные данные
// В данном случае - из UTF-8
Source := UTF8ToString(SourceBytes);

// Исходные данные больше не нужны - затираем
SecureFree(SourceBytes);

// Как-то использовали исходные данные
ShowMessage(Source);

// Исходные данные больше не нужны - затираем
SecureFree(Source);

finally
// На всякий случай (исключение) - затёрли и освободили всё
SecureFree(Source);
SecureFree(SourceBytes);
SecureFree(EncryptedBytes);
SecureFree(Key);
SecureFree(Salt);
end;
end;

2. Обмен зашифрованными данными между Delphi и PHP:

uses
EEncrypt, // для всех Twofish функций и SecureFree
EEncoding, // для Base64EncodeToString
ECore; // для ShellExec

procedure TForm1.Button1Click(Sender: TObject);
const
// Секретный ключ, известный обоим сторонам
// Должен быть тем же самым, что и в PHP скрипте
// Это просто случайный набор байт, полученный вызовом TwofishInitSessionKeyRAW
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
SecretKey: TTwofishRAWKey =
(160, 22, 228, 9, 73, 192, 173, 149,
154, 19, 115, 215, 74, 36, 20, 202,
178, 26, 103 , 47, 51, 4, 144, 20,
73, 153, 49, 160, 192, 25, 20, 114);
var
Key: TTwofishKey; // Оптимизированный ключ
IV: TTwofishInitVector; // Вектор инициализации
Text: String; // Исходный текст
TextRAW: RawByteString; // Байтовое представление исходного текста
EncryptedText: RawByteString; // Двоичное представление зашифрованного текста
EncodedIV: String; // Кодированный в текст вектор инициализации
EncodedText: String; // Кодированный в текст зашифрованный текст
URL: String; // URL для вызова PHP
ReplyRAW: RawByteString; // Ответ от PHP (байты)
Reply: String; // Ответ от PHP (текст)
begin
// Готовим буферы
FillChar(Key, SizeOf(Key), 0);
FillChar(IV, SizeOf(IV), 0);
try

// Откуда-то получаем исходный текст
Text := 'Привет!';

// Конвертируем текст в байты. В данном случае - UTF-8
TextRAW := UTF8Encode(Text);

// Затрём исходный текст
SecureFree(Text);

// Оптимизируем ключ
Key := TwofishInitKey(SecretKey);

// Сгенерируем случайные байты для использования в качестве вектора инициализации
IV := TwofishInitIV;

// Шифруем открытый текст
// Поскольку используется вектор инициализации,
// то шифрование происходит в режиме CBC
EncryptedText := TwofishEncrypt(Key, TextRAW, @IV);

// Затрём исходный текст и ключ
SecureFree(TextRAW);

// Кодируем в текст двоичные данные
EncodedIV := Base64EncodeToString(@IV, SizeOf(IV));
EncodedText := Base64EncodeString(EncryptedText);

// Опционально
SecureFree(EncryptedText);

// Формируем URL для вызова PHP скрипта
// URLEncode нужно для экранирования символа '+'
// Если вместо Base64 вы будете кодировать в HEX, то URLEncode не нужно
URL := Format('http://localhost/test.php?iv=%s&text=%s', [URLEncode(EncodedIV), URLEncode(EncodedText)]);

// Опционально
SecureFree(EncodedIV);
SecureFree(EncodedText);

// Передаём в PHP
if not InitWebTools then
RaiseLastOSError;
try
ReplyRAW := InternetGet(URL, [], []);
finally
DoneWebTools;
end;

// PHP-скрипт ничего не вернул?
if ReplyRAW = '' then
Abort;

// Опционально
SecureFree(URL);

// Декодируем текст в байты
ReplyRAW := Base64DecodeString(Trim(String(ReplyRAW)));

// Расшифровываем ответ
TextRAW := TwofishDecrypt(Key, ReplyRAW, @IV);

// Удаляем ключ и вектор
SecureFree(Key);
SecureFree(IV);

// Опционально
SecureFree(ReplyRAW);

// Конвертируем двоичное представление в текст, в данном случае - UTF-8
Text := UTF8ToString(TextRAW);

// Зануляем ответ
SecureFree(TextRAW);

// Как-то используем ответ
ShowMessage(Text);
// Покажет:
// 'Hello from PHP: Привет'

// Зануляем ответ
SecureFree(Text);

finally
// На всякий случай (исключение) - затираем всё
SecureFree(Text);
SecureFree(TextRAW);
SecureFree(Key);
SecureFree(IV);
SecureFree(EncryptedText);
SecureFree(EncodedIV);
SecureFree(EncodedText);
SecureFree(URL);
SecureFree(ReplyRAW);
SecureFree(Reply);
end;
end;
<?php

// Эти функции необходимы, потому что MCrypt использует дополнение нулями вместо PKCS#5
// OpenSSL поддерживает PKCS#5, но не поддерживает Twofish

// Дополнение данных по PKCS#5
function pkcs5_pad($text, $blocksize = 16) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}

// Обрезка PKCS#5
function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}

// Секретный ключ, известный обоим сторонам
// Должен быть тем же самым, что и в Delphi
// Это просто случайный набор байт, полученный вызовом random_bytes(32)
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
$Key = pack('C*',
160, 22, 228, 9, 73, 192, 173, 149,
154, 19, 115, 215, 74, 36, 20, 202,
178, 26, 103 , 47, 51, 4, 144, 20,
73, 153, 49, 160, 192, 25, 20, 114);

// Читаем переданные данные (вектор инициализации и зашифрованный текст)
$EncodedIV = $_GET['iv'];
$EncodedText = $_GET['text'];

// Декодируем текст в байты
$IV = base64_decode($EncodedIV);
$EncryptedText = base64_decode($EncodedText);

// Расшифровываем текст (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$Text = mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedText, MCRYPT_MODE_CBC, $IV);

// Обрезаем данные до их реального размера
$Text = pkcs5_unpad($Text);

// Что-то делаем с открытым текстом из Delphi
$Text = 'Hello from PHP: ' . $Text;

// Дополняем данные до размера, кратного блоку шифра
$Text = pkcs5_pad($Text);

// Зашифровываем ответ (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$EncryptedText = mcrypt_encrypt(MCRYPT_TWOFISH, $Key, $Text, MCRYPT_MODE_CBC, $IV);

// Кодируем зашифрованные байты в текст
$EncodedText = base64_encode($EncryptedText);

// Возвращаем ответ в Delphi
echo($EncodedText);

3. Хранение файла в зашифрованном виде:

uses
EEncrypt, // для всех Twofish функций и SecureFree
EEncoding, // для HexEncodeToString/HexDecodeString
EConfig, // для RegKeyWrite/RegKeyRead
ECompatibility; // для FileToString/StringToFile

procedure TForm1.Button1Click(Sender: TObject);
var
RAWKey: TTwofishRAWKey; // Ключ для шифрования и дешифрования (исходный)
Key: TTwofishKey; // Ключ для шифрования и дешифрования (оптимизированный)
IV: TTwofishInitVector; // Вектор инициализации
Content: RawByteString; // Исходные байты файла
EncryptedData: RawByteString; // Зашифрованный файл
DataClear: RawByteString; // Для шифрования ключа шифрования
DataEncrypted: RawByteString; // Для шифрования ключа шифрования
DataStr: String; // Строковое представление зашифрованного ключа шифрования
begin
// Обнуляем все буферы
FillChar(RAWKey, SizeOf(RAWKey), 0);
FillChar(Key, SizeOf(Key), 0);
FillChar(IV, SizeOf(IV), 0);

// Шаг 1: шифрование файла случайным ключём
try

// Создали случайный ключ
RAWKey := TwofishInitSessionKeyRAW;

// Оптимизировали ключ
Key := TwofishInitKey(RAWKey);

// Загружаем файл в двоичную строку
Content := FileToString(ParamStr(0));
// В Content будет 'MZP'#0#2#0#0#0...

// Создали случайный набор байтов для использования в качестве вектора
IV := TwofishInitIV;

// Шифруем данные (файл)
EncryptedData := TwofishEncrypt(Key, Content, @IV);

// Чистим и удаляем ненужные данные
SecureFree(Content);
SecureFree(Key);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(Content);
SecureFree(Key);
end;

// Далее у нас есть:
// RAWKey - (секретный) ключ, которым мы шифровали данные
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные

// Шаг 2: защита ключа шифрования
try

// Конвертируем ключ шифрования в RawByteString для удобства
DataClear := RAWToString(@RAWKey, SizeOf(RAWKey));

// Чистим исходный ключ шифрования
SecureFree(RAWKey);

// Шифруем ключ шифрования
DataEncrypted := DataProtect(DataClear);

// Чистим исходный ключ шифрования
SecureFree(DataClear);

// Преобразовали зашифрованный ключ в текст
DataStr := HexEncodeToString(Pointer(DataEncrypted), Length(DataEncrypted));

// Опционально
SecureFree(DataEncrypted);

// Сохранили зашифрованный ключ в реестре
// Мы можем это сделать, поскольку ключ сможет расшифровать только этот же пользователь
RegKeyWrite(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'EncryptionKey', DataStr);

// Опционально
SecureFree(DataStr);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RAWKey);
SecureFree(DataClear);
SecureFree(DataEncrypted);
SecureFree(DataStr);
end;

// Далее у нас есть:
// Зашифрованный (секретный) ключ в реестре
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные
// Если нам нужно сохранить зашифрованные данные на диск, то мы сохраняем IV и EncryptedData
// Расшифровать данные сможет только текущий пользователь и никто иной

// Шаг 3: расшифровка ключа шифрования
try

// Прочитали зашифрованный ключ шифрования (в виде текста)
DataStr := RegKeyRead(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'EncryptionKey', '');

// Декодировали текст в двоичные данные
DataEncrypted := HexDecodeString(DataStr);

// Опционально
SecureFree(DataStr);

// Расшифровали зашифрованный ключ
DataClear := DataUnprotect(DataEncrypted);

// Опционально
SecureFree(DataEncrypted);

// Подготовили расшифрованный ключ
Assert(Length(DataClear) = SizeOf(RAWKey));
RAWFromString(DataClear, @RAWKey);

// Удалили копию расшифрованного ключа
SecureFree(DataClear);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(DataStr);
SecureFree(DataEncrypted);
SecureFree(DataClear);
end;

// Далее у нас есть:
// RAWKey - (секретный) ключ, которым мы шифровали данные
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные

// Шаг 4: расшифровываем файл
try

// Оптимизировали ключ
Key := TwofishInitKey(RAWKey);

// Чистим ненужный ключ
SecureFree(RAWKey);

// Расшифровываем данные (файл)
// Здесь Key и IV должны быть теми же самыми, что использовались для шифрования
Content := TwofishDecrypt(Key, EncryptedData, @IV);

// Чистим ненужный ключ
SecureFree(Key);

// Опционально
SecureFree(EncryptedData);

// Сохранили расшифрованные данные в файл
StringToFile(ParamStr(0) + '.copy', Content);
// Здесь файл Project1.exe.copy получится в точности равным Project1.exe

// Затрём исходные данные
SecureFree(Content);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RAWKey);
SecureFree(Key);
SecureFree(EncryptedData);
SecureFree(Content);
end;
end;

Асимметричная криптография

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

Управление ключами

В EurekaLog ключи RSA хранятся в записи TRSAKey, которая имеет два поля: PublicKey для хранения открытого ключа и PrivateKey для хранения закрытого (секретного) ключа. Новую пару ключей можно сгенерировать функцией RSAGenKey (выполняется очень долго; читай: 5-15 секунд). Как правило, ключи не генерируются в программах, а загружаются уже готовые (заранее сгенерированные). Для сохранения загрузки ключей в EurekaLog есть функции RSALoad/SavePublic/PrivateKey, например: RSALoadPrivateKey. Для импорта/экспорта EurekaLog поддерживает несколько форматов, описываемых TRSAExport/TRSAImport:

  • rsBLOB — это двоичное представление ключа с заголовком. В качестве заголовка используются структуры PUBLICKEYBLOB/PRIVATEKEYBLOB Microsoft. Little-Endian.
  • rsDER — это двоичное представление ключа, кодированное в контейнер ASN.1 без опционального заголовка (формат PKCS#1), Big-Endian. При загрузке опциональный заголовок ASN.1 от PKCS#8 (например, после экспорта из PHP/OpenSSL) будет пропущен. В CryptoAPI соответствует RSA_CSP_PUBLICKEYBLOB/PKCS_RSA_PRIVATE_KEY. Как правило, такой формат сохраняется в файлы с расширением .der.
  • rsPEM — это текстовое представление ключа. По сути, это тот же rsDER, но кодированный в Base64. Как правило, такой формат сохраняется в файлы с расширением .pem, .key, .cert, .cer или .crt. Файлы в формате PKCS#1 используют заголовки -----BEGIN/END RSA PRIVATE KEY-----, -----BEGIN/END RSA PUBLIC KEY-----, а файлы в формате PKCS#8 используют заголовки -----BEGIN/END PRIVATE KEY-----, -----BEGIN/END PUBLIC KEY-----.

Зашифрованные ключи PKCS#1/PKCS#8 в EurekaLog не поддерживаются. Форматы контейнеров PKCS#7 и PKCS#12 в EurekaLog не поддерживаются. PKCS#8 поддерживается EurekaLog только на импорт (см. ниже).

Например, один и тот же публичный ключ экспортируется следующим образом:

  • rsBLOB (заголовок PUBLICKEYBLOB/PRIVATEKEYBLOB + little-endian, ключ $25 $17 $B4 $A0 … $96 $B9 $9C $E7 начинается с $15/21-го байта и до конца):
    rsBLOB
  • rsDER (ASN.1 контейнер PKCS#1 + big-endian, т.е. тот же самый ключ $E7 $9C $B9 $96 … $A0 $B4 $17 $25 начинается с 10-го байта и заканчивается на 6-м байте с конца):
    rsPEM
  • rsPEM (Base64-кодированный контейнер ASN.1 PKCS#1):
    -----BEGIN RSA PUBLIC KEY-----
    MIICCgKCAgEA55y5ll1KryRC7umxntWX7t3zOP3qUVxQo7gin3sA1dePyzLxTxtE
    47R+/sqkgFygXdlBqnmjbwu60kU2Zd7k7QFGhZWqfPcAYI3xd660vUPnmXK7n2R1
    3AtF2BW/5MqIH7D3ddjLCt5CoUn6KRZSuz+pySDpuquKerRB5Gq/0WjUG2IIcQXU
    Z1i4qMicPhbOJH76rFPgRngBuvJtS0UCBKx4YOlK0q1JUUJ1leSGp2gAjYGrD7fN
    SOU8r70a97NDu4UblmsS9zW29OHAEF7jNFsVNVBU78P/XZ4hmL41gaPRGws3HXfA
    vGbVattUzHTHsHMJeRLoiPAgak3TqAM2px7qOcNNN8FB91XbnxzvPARfDrBMbpc4
    OcWmDSMuc1RGI/mQCIlGRvA2nhD7Dfu3L5sxnrjjOC+LLpIVsGe5+cs1ZkfD7kII
    AzV/MXXNlx366n/Z1+u97VocmvHcqVCl/s9AMqdXflzAYD+9p7bXhJdP9XfOXf9z
    zCyPBK/Iyk+B4lRR9cmuBW7FAq1JM3PZWZ2mEx0fgrL8M0w5cf2Ts84XtNIEFDa6
    MFOe48sJfDIiPPw4ePohSuYpAY71Du2cQe87VQAf/caclWsrFplItilN93Xx5kQW
    5S16HHLc7A+EKEaNBnUsNl+n0/99jjfHA9PAqxFVaVT68X9eSKC0FyUCAwEAAQ==
    -----END RSA PUBLIC KEY-----

Пример создания и экспорта нового ключа:

  1. На Delphi (PKCS#1):
    uses
    EEncrypt; // для RSA функций

    procedure TForm1.Button1Click(Sender: TObject);
    var
    Key: TRSAKey;
    begin
    // Создали новую пару ключей (несколько секунд)
    Key := RSAGenKey;
    try

    // Экспорт обоих ключей во все форматы (для примера)
    RSASavePublicKey(Key, 'C:Documentspublic.blob', rsBLOB);
    RSASavePublicKey(Key, 'C:Documentspublic.der', rsDER);
    RSASavePublicKey(Key, 'C:Documentspublic.pem', rsPEM);

    RSASavePrivateKey(Key, 'C:Documentsprivate.blob', rsBLOB);
    RSASavePrivateKey(Key, 'C:Documentsprivate.der', rsDER);
    RSASavePrivateKey(Key, 'C:Documentsprivate.pem', rsPEM);

    finally
    // Удалили ключи
    SecureFree(Key);
    end;
    end;
  2. На PHP (PKCS#8):
    <?php

    // Создали новую пару ключей (выполняется несколько секунд)
    $config = array(
    "private_key_bits" => 4096,
    "private_key_type" => OPENSSL_KEYTYPE_RSA,
    "encrypt_key" => false
    );
    $privateKey = openssl_pkey_new($config);
    $publicKey = openssl_pkey_get_public($privateKey);

    // Сохраняем ключи в файлы
    openssl_pkey_export($privateKey, $PEM, null, $config);
    file_put_contents('./private.pem', $PEM);
    file_put_contents('./public.pem', $publicKey['key']);
  3. На OpenSSL (PKCS#8):
    openssl genpkey -out private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096
    openssl rsa -in private.pem -out public.pem -outform pem -pubout

Если вы хотите обмениваться ключами между Delphi/EurekaLog, Windows/WinCrypt и PHP/OpenSSL — вам нужно использовать формат с кодированием в контейнер ASN.1 (т.е. двоичный DER или текстовый PEM). Здесь есть один подводный камень:

  1. Windows/WinCrypt экспортируют/импортируют только сами ключи в контейнер ASN.1. Этот формат называется PKCS#1, в текстовом виде (PEM) его можно отличить по комментарию вида -----BEGIN RSA PUBLIC KEY-----. Если вы откроете сохранённый PEM/DER файл в любом ASN.1 редакторе/декодере (или используете команду openssl asn1parse -in private.pem), то увидите такую картину:
    SEQUENCE (9 elem)
    INTEGER 0
    INTEGER (4096 bit) 758666102228921792910938751013886686183781000609266742264329283135491…
    INTEGER 65537
    INTEGER (4096 bit) 626038158727742962915964533848528190012037116635627896022932505790124…
    INTEGER (2048 bit) 265173868639039499374763037151257212413358779720904507462546669893261…
    INTEGER (2048 bit) 286101381754789259717271171537486143896554619163648315398099842455950…
    INTEGER (2048 bit) 241273140164272477344356926702923044634459679795497746005945617372402…
    INTEGER (2047 bit) 117977475958972484914769571551956345862709440207784850140129213152449…
    INTEGER (2047 bit) 114103066753170488160676998988478007480145103422326665392933549037964…

    Здесь 8 полей описывают различные компоненты ключа (модуль, экспоненту и т.д.).

  2. PHP/OpenSSL перед ключом добавляют заголовок с указанием алгоритма. Этот формат называется PKCS#8, в текстовом виде (PEM) его можно отличить по комментарию вида -----BEGIN PRIVATE KEY-----. Если вы откроете сохранённый файл PEM/DER в любом ASN.1 редакторе/декодере, то увидите такую картину:
    SEQUENCE (3 elem)
    INTEGER 0
    SEQUENCE (2 elem)
    OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1)
    NULL
    OCTET STRING (2349 byte) 30820929020100028202010098D61F2FBA3EC958DB082F286781EE7CC258ADCE2B0A…
    SEQUENCE (9 elem)
    INTEGER 0
    INTEGER (4096 bit) 758666102228921792910938751013886686183781000609266742264329283135491…
    INTEGER 65537
    INTEGER (4096 bit) 626038158727742962915964533848528190012037116635627896022932505790124…
    INTEGER (2048 bit) 265173868639039499374763037151257212413358779720904507462546669893261…
    INTEGER (2048 bit) 286101381754789259717271171537486143896554619163648315398099842455950…
    INTEGER (2048 bit) 241273140164272477344356926702923044634459679795497746005945617372402…
    INTEGER (2047 bit) 117977475958972484914769571551956345862709440207784850140129213152449…
    INTEGER (2047 bit) 114103066753170488160676998988478007480145103422326665392933549037964…

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

Разные библиотеки поддерживают разные форматы. Если вы попробуете импортировать ключ, сохранённый в неподдерживаемом формате, то можете получить ошибки вроде таких:

CRYPT_E_ASN1_BADTAG (8009310B): ASN1 bad tag value met
openssl_pkey_get_private: error:0D0680A8:asn1 encoding routines:ASN1_CHECK_TLEN:wrong tag

EurekaLog сохряняет в PKCS#1, но умеет загружать PKCS#1 и PKCS#8. OpenSSL сохраняет в PKCS#8, но умеет загружать PKCS#8 и PKCS#1.

ВАЖНО
OpenSSL поддерживает PKCS#1/PKCS#8 в PEM только при наличии правильного комментария! Т.е. файл PKCS#1 должен начинаться с -----BEGIN RSA PRIVATE KEY----- или -----BEGIN RSA PUBLIC KEY-----, а файл PKCS#8 должен начинаться с -----BEGIN PRIVATE KEY----- или -----BEGIN PUBLIC KEY-----.

P.S. Вы можете сконвертировать PKCS#1 (Windows/WinCrypt/EurekaLog) в PKCS#8 (PHP/OpenSSL):

openssl pkcs8 -topk8 -inform pem -in private.pem -outform pem -nocrypt -out private2.pem
openssl rsa -RSAPublicKey_in -in public.pem -pubout -out public2.pem

и наоборот:

openssl rsa -inform pem -in private.pem -outform pem -out private2.pem
openssl rsa -pubin -in public.pem -RSAPublicKey_out -out public2.pem

P.P.S. В последних версиях OpenSSL вы также можете использовать двоичный формат BLOB:
Преобразовать PEM в BLOB:

openssl rsa -inform PEM -in private.pem -outform "MS PRIVATEKEYBLOB" -out private.blob

Преобразовать BLOB в PEM:

openssl rsa -inform "MS PRIVATEKEYBLOB" -in private.blob -outform PEM -out private.pem

Для публичного ключа нужно поменять формат на "MS PUBLICKEYBLOB", либо вы можете конвертировать закрытый ключ, а потом получить публичный ключ из закрытого.

Суммируя вышесказанное, мы рекомендуем:

  1. Использовать утилиту EurekaLog Crypto Helper для создания пары RSA ключей:
    1. Запустить Пуск / Программы / EurekaLog / Tools / EurekaLog Crypto Helper
    2. Перейти на вкладку Keys
    3. Перейти на вкладку RSA
    4. Нажать кнопку Create New
    5. Сохранить закрытый и открытый ключи в файлы private.pem и public.pem соответственно. Это сохранит ключи в формате PKCS#1.

    Альтернативно, вы можете использовать такой код:

    uses
    EEncrypt; // для RSA функций

    procedure TForm1.Button1Click(Sender: TObject);
    var
    Key: TRSAKey;
    begin
    // Создали новую пару ключей (несколько секунд)
    Key := RSAGenKey;
    try

    // Экспорт обоих ключей в PKCS#1
    RSASavePublicKey (Key, 'C:Documentspublic.pem', rsPEM);
    RSASavePrivateKey(Key, 'C:Documentsprivate.pem', rsPEM);

    finally
    // Удалили ключи
    SecureFree(Key);
    end;
    end;
  2. Импортировать ключи в Delphi (EurekaLog умеет загружать PKCS#1):
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#1 PEM
    RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#1 PEM
    RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
  3. Импортировать ключи в PHP (OpenSSL в PHP умеет работать с PKCS#1):
    <?php

    $PrivateKey = <<<EOD
    -----BEGIN RSA PRIVATE KEY-----
    MIIJKgIBAAKCAgEA5CBBmb8QlDVt7uqKZPW6I/GcjyzDg+7VuZd5OcQXVpglsWoa
    ...
    aNz1gCLcqrQiTXHTVg821kYszBDySjfQGJQ3JJhf1/9XGcVjcopbWWeeNpHs5w==
    -----END RSA PRIVATE KEY-----
    EOD;

    // Или:
    // $PrivateKey = 'file:///var/www/private.pem';

    $PrivateKey = openssl_pkey_get_private($PrivateKey);
    if (!PrivateKey) {
    echo('openssl_pkey_get_private: ' . openssl_error_string());
    die();
    }
    <?php

    $PublicKey = <<<EOD
    -----BEGIN RSA PUBLIC KEY-----
    MIICCgKCAgEA5CBBmb8QlDVt7uqKZPW6I/GcjyzDg+7VuZd5OcQXVpglsWoauYvq
    ...
    f69nl8KyfHhsqffkDeDIaA73hspgFM5bh2zGdj4n8101bjHRu8N35qECAwEAAQ==
    -----END RSA PUBLIC KEY-----
    EOD;

    // Или:
    // $PublicKey = 'file:///var/www/public.pem';

    $PublicKey = openssl_pkey_get_public($PublicKey);
    if (!PublicKey) {
    echo('openssl_pkey_get_public: ' . openssl_error_string());
    die();
    }

    Здесь важно, чтобы заголовки были указаны правильно. Т.е. должно быть -----BEGIN RSA PRIVATE KEY----- и -----BEGIN RSA PUBLIC KEY-----.

Альтернативно, вы можете:

  1. Скачать OpenSSL для Windows.
  2. Создать пару ключей, эти команды создадут два файла private.pem и public.pem в формате PKCS#8 PEM:
    openssl genpkey -out private.pem -algorithm RSA -pkeyopt rsa_keygen_bits:4096
    openssl rsa -in private.pem -out public.pem -outform pem -pubout
  3. Импортировать ключи в Delphi (EurekaLog умеет загружать PKCS#8):
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#8 PEM
    RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
    var
    RSAKey: TRSAKey;
    begin
    // Импорт PKCS#8 PEM
    RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
  4. Импортировать ключи в PHP (OpenSSL в PHP умеет работать с PKCS#8):
    <?php

    $PrivateKey = <<<EOD
    -----BEGIN PRIVATE KEY-----
    MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u
    ...
    X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK
    N9AYlDckmF/X/1cZxWNyiltZZ542kezn
    -----END PRIVATE KEY-----
    EOD;

    // Или:
    // $PrivateKey = 'file:///var/www/private.pem';

    $PrivateKey = openssl_pkey_get_private($PrivateKey);
    if (!PrivateKey) {
    echo('openssl_pkey_get_private: ' . openssl_error_string());
    die();
    }
    <?php

    $PublicKey = <<<EOD
    -----BEGIN PUBLIC KEY-----
    MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5CBBmb8QlDVt7uqKZPW6
    ...
    v4sIP6NMmNKN8TwtqUKxcjZJMrVhjPJWf69nl8KyfHhsqffkDeDIaA73hspgFM5b
    h2zGdj4n8101bjHRu8N35qECAwEAAQ==
    -----END PUBLIC KEY-----
    EOD;

    // Или:
    // $PublicKey = 'file:///var/www/public.pem';

    $PublicKey = openssl_pkey_get_public($PublicKey);
    if (!PublicKey) {
    echo('openssl_pkey_get_public: ' . openssl_error_string());
    die();
    }

    Здесь важно, чтобы заголовки были указаны правильно. Т.е. должно быть -----BEGIN PRIVATE KEY----- и -----BEGIN PUBLIC KEY-----.

Примечание: вызов openssl_pkey_get_private/openssl_pkey_get_public является опциональным. Если переменная ключа содержит сам ключ в формате PEM, то его можно передавать напрямую в функции OpenSSL. В данном случае функции openssl_pkey_get_private/openssl_pkey_get_public вызываются только для примера и проверки правильности указания ключа.

Ассиметричное шифрование

Для шифрования и расшифровки в EurekaLog есть функции RSAEncrypt и RSADecrypt, которые используются аналогично функциям симметричного шифрования выше. Есть только несколько отличий:

  1. Поскольку асимметричное шифрование использует два разных ключа, оно используется в сценариях «отправитель-получатель» и не используется, когда шифрует и расшифровывает данные одно и то же лицо.
  2. Так как асимметричное шифрование работает очень медленно, то оно никогда не применяется к самим открытым данным. Вместо этого открытые данные шифруются любым симметричным шифром со случайным ключом (так называемый «сессионный ключ»), а затем симметричный ключ шифруется асимметричным шифрованием.
  3. Для шифрования используется открытый ключ, а для расшифровки — закрытый. Т.е. кто угодно может зашифровать данные открытым ключом получателя, при этом расшифровать данные сможет только получатель. Так и обеспечивается секретность.

Функции RSAEncrypt/RSADecrypt работают с little-endian данными и используют алгоритм дополнения PKCS#1 Тип 2.

Шифрование файла на диске:

uses
EEncrypt; // для всех RSA функций и SecureFree

procedure TForm1.Button1Click(Sender: TObject);
var
// Случайный симметричный ключ для шифрования данных
SessionKey: TTwofishKey;
SessionKeyRAW: TTwofishRAWKey;
// Асимметричный ключ для шифрования симметричного ключа
RSAKey: TRSAKey;
// Открытые данные для шифрования
Data: TMemoryStream;
// Зашифрованные данные
EncryptedData: TEncryptBuffer;
// Поток для сохранения данных
FS: TFileStream;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(EncryptedData, SizeOf(EncryptedData), 0);

try
// Создадим случайный симметричный ключ
SessionKeyRAW := TwofishInitSessionKeyRAW;
SessionKey := TwofishInitKey(SessionKeyRAW);

// FS используется дважды:
// 1). Для записи зашифрованного симметричного ключа
// 2). Для записи самиз зашифрованных данных

FS := nil;
try

// Шаг 1: шифруем ключ шифрования

// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try

// Шифруем симметричный ключ SessionKeyRAW в буфер EncryptedData
EncryptedData.cbData := SizeOf(SessionKeyRAW);
RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData);

finally
// Как только мы его зашифровали - нам больше не нужен асимметричный ключ
SecureFree(RSAKey);
// Также не нужна и исходная форма ключа
SecureFree(SessionKeyRAW);
end;
try

// Здесь: EncryptedData хранит зашифрованный симметричный ключ

// Сохраним зашифрованный ключ шифрования в сам выходной файл
FS := TFileStream.Create('C:DocumentsEncryptedData.bin', fmCreate or fmShareExclusive);
FS.WriteBuffer(EncryptedData.cbData, SizeOf(EncryptedData.cbData));
FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData);

// Не закрываем FS, мы ещё не записали туда сами данные...

finally
// Удаляем зашифрованный ключ
SecureFree(EncryptedData);
end;

// Далее, у нас есть симметричный ключ шифрования SessionKey и открытый выходной файл FS


// Шаг 2: шифруем данные

Data := TMemoryStream.Create;
try

// Загрузим открытый текст для шифрования
Data.LoadFromFile('C:DocumentsText.txt');

// Шифруем файл
// Поскольку ключ шифрования - случайный, то вектор инициализации не используется
// Поэтому шифрование происходит в режиме ECB
EncryptedData.cbData := Cardinal(Data.Size);
TwofishEncrypt(SessionKey, Data.Memory, Pointer(EncryptedData.pbData), EncryptedData.cbData);

// Ключ больше не нужен
SecureFree(SessionKey);

// Сохраняем зашифрованные данные в файл
FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData);

finally
// Удаляем данные
SecureFree(Data);
SecureFree(EncryptedData);
end;

finally
// Закрываем файл
FreeAndNil(FS);
end;

// Теперь файл C:DocumentsEncryptedData.bin полностью готов
// В нём лежит зашифрованный (случайный) симметричный ключ, а также зашифрованный файл C:DocumentsText.txt
// Его сможет расшифровать только тот, у кого есть закрытый асимметричный ключ, соответствующий использованному открытому асимметричному ключу
// Т.е. файл можно передать получателю любым способом, в том числе - по открытым каналам связи

finally
// Чистим всё
SecureFree(SessionKey);
end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
// Случайный симметричный ключ для шифрования данных
SessionKey: TTwofishKey;
// Асимметричный ключ для шифрования симметричного ключа
RSAKey: TRSAKey;
// Открытые данные для шифрования
Data: TMemoryStream;
// Расшифрованные данные
DecryptedData: TEncryptBuffer;
// Поток для сохранения данных
FS: TFileStream;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(DecryptedData, SizeOf(DecryptedData), 0);

// Открываем зашифрованный файл
FS := TFileStream.Create('C:DocumentsEncryptedData.bin', fmOpenRead or fmShareDenyWrite);
try

try
// Шаг 1: расшифровываем сессионный ключ

// Сначала читаем из файла зашифрованный (случайный) симметричный ключ
FS.ReadBuffer(DecryptedData.cbData, SizeOf(DecryptedData.cbData));
DecryptedData.pbData := AllocMem(DecryptedData.cbData);
try
FS.ReadBuffer(DecryptedData.pbData^, DecryptedData.cbData);

// Расшифровываем симметричный ключ с помощью закрытого асимметричного ключа
RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
try
EEncrypt.RSADecrypt(RSAKey, DecryptedData);
finally
SecureFree(RSAKey);
end;

// Инициализируем симметричный ключ
Assert(DecryptedData.cbData = SizeOf(TTwofishRAWKey));
SessionKey := TwofishInitKey(TTwofishRAWKey(Pointer(DecryptedData.pbData)^));

finally
// Удаляем ненужные данные
SecureFree(DecryptedData);
end;

// Теперь у нас есть симметричный ключ SessionKey, которым мы можем расшифровать файл

// Шаг 2: расшифровываем сами данные

Data := TMemoryStream.Create;
try
// Остаток в файле - это сам зашифрованный файл
Data.CopyFrom(FS, FS.Size - FS.Position);

// Расшифровываем файл
DecryptedData.cbData := Cardinal(Data.Size);
TwofishDecrypt(SessionKey, Data.Memory, Pointer(DecryptedData.pbData), DecryptedData.cbData);

// Ключ больше не нужен
SecureFree(SessionKey);

// Сохраняем расшифрованный результат в файл
FreeAndNil(FS);
FS := TFileStream.Create('C:DocumentsText2.txt', fmCreate or fmShareExclusive);
try
FS.WriteBuffer(DecryptedData.pbData^, DecryptedData.cbData);
finally
FreeAndNil(FS);
end;

// Данные больше не нужны
SecureFree(DecryptedData);

// Здесь: Text2.txt должен стать точной копией Text.txt

finally
SecureFree(Data);
end;

finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(SessionKey);
SecureFree(DecryptedData);
end;
finally
FreeAndNil(FS);
end;
end;

Цифровая подпись

Когда для шифрования используется закрытый ключ, а для расшифровки используется открытый — это называется «цифровая подпись». Поскольку расшифровать данные может любой (т.к. открытый ключ есть у всех), то секретность данных таким способом не обеспечивается. Но зато, если данные удаётся расшифровать чьим-то открытым ключом — мы можем быть уверены, что данные были зашифрованны именно им (ибо секретный закрытый ключ есть только у этого лица). Таким образом, мы можем проверить аутентичность данных.

При этом как правило сами данные не шифруются. Вместо этого от данных вычисляют хэш и его шифруют. Для подписи произвольных данных и её проверки в EurekaLog есть функции RSASign (использует закрытый ключ) и RSAVerify (использует открытый ключ). Функции цифровой подписи EurekaLog используют SHA1 со схемой EMSA-PKCS1 для дополнения данных.

Результирующая цифровая подпись представляет собой непрозрачный набор байт произвольной длины. Если вы захотите обмениваться цифровой подписью с другими окружениями — нужно помнить, что Windows/Delphi используют little endian порядок байт, в то время как некоторые другие окружения (например, .NET или PHP) используют big endian. Поэтому в некоторых случаях порядок байт цифровой подписи нужно обратить.

Например:

Запрос лицензии от PHP-скрипта:

uses
EEncrypt, // для всех RSA функций и SecureFree
EEncoding, // для Base64
EJSON, // для JSON функций
EWebTools; // для интернет-функций

procedure TForm1.Button1Click(Sender: TObject);
var
// Данные для отправки PHP-скрипту
JSON, JSONRequest, JSONUser: IJSONValues;
JSONText: String;
JSONRAW: RawByteString;
// Случайный симметричный ключ для шифрования данных (сессионный ключ)
SessionKey: TTwofishKey;
SessionKeyRAW: TTwofishRAWKey;
// Асимметричный ключ для шифрования сессионного ключа
RSAKey: TRSAKey;
// Зашифрованные данные
EncryptedData: TEncryptBuffer;
// Текстовое представление зашифрованных данных
EncodedKey, EncodedData: String;
// URL для вызова PHP
URL: String;
// Ответ от PHP (байты)
ReplyRAW: RawByteString;
// Текстовое представление лицензии
EncodedLicense: String;
// Текстовое представление цифровой подписи
EncodedSignature: String;
// Лицензия (байты)
License: RawByteString;
// Цифровая подпись (байты)
Signature: RawByteString;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(EncryptedData, SizeOf(EncryptedData), 0);

try

// Шаг 1: готовим данные для отправки PHP-скрипту
JSON := JSONCreate;
JSONRequest := JSONCreate;
JSONUser := JSONCreate;

JSONRequest['version'] := 1;
JSONRequest['type'] := 'license';
JSONRequest['scope'] := 'installer';

JSONUser['login'] := 'input-from-edit1';
JSONUser['password'] := 'input-from-edit2';

JSON['app'] := 'MyApp';
JSON['version'] := GetModuleVersion(GetModuleName(HInstance));
JSON['date'] := Now;
JSON['request'] := JSONRequest;
JSON['user'] := JSONUser;

JSONText := JSON.ToString;
Finalize(JSONUser); // не обязательно
Finalize(JSONRequest); // не обязательно
Finalize(JSON); // не обязательно
(*

Здесь JSONText содержит:

{
"app": "MyApp",
"version": "1.0.0.0",
"date": "2021.06.25 14:04:21",
"request": {
"version": 1,
"type": "license",
"scope": "installer"
}
"user": {
"login": "input-from-edit1",
"password": "input-from-edit2"
}
}

*)
JSONRAW := UTF8Encode(JSONText);
SecureFree(JSONText);

// Здесь JSONRAW содержит байты для отправки PHP-скрипту


// Шаг 2: шифруем данные запроса и отправляем PHP-скрипту

// Создадим случайный симметричный ключ
SessionKeyRAW := TwofishInitSessionKeyRAW;
SessionKey := TwofishInitKey(SessionKeyRAW);

// Шаг 2а: шифруем ключ шифрования

// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try

// Шифруем симметричный ключ SessionKeyRAW в буфер EncryptedData
EncryptedData.cbData := SizeOf(SessionKeyRAW);
RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData);
try

// Как только мы его зашифровали - нам больше не нужен асимметричный ключ
SecureFree(RSAKey);
// Также не нужна и исходная форма ключа
SecureFree(SessionKeyRAW);

// Кодируем зашифрованные данные в текст
EncodedKey := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData);

// Удаляем ненужный данные
SecureFree(EncryptedData);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(EncryptedData);
end;

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RSAKey);
SecureFree(SessionKeyRAW);
end;

// Здесь: EncodedKey хранит зашифрованный симметричный ключ (в текстовом виде)
// Далее, у нас есть симметричный ключ шифрования SessionKey и данные для отправки JSONRAW

// Шаг 2б: шифруем данные запроса

// Поскольку ключ шифрования - случайный, то вектор инициализации не используется
// Поэтому шифрование происходит в режиме ECB
EncryptedData.cbData := Length(JSONRAW);
TwofishEncrypt(SessionKey, Pointer(JSONRAW), Pointer(EncryptedData.pbData), EncryptedData.cbData);

try
// Ключ больше не нужен
SecureFree(SessionKey);

// Кодируем зашифрованные данные в текст
EncodedData := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData);

// Удаляем ненужные данные
SecureFree(EncryptedData);

// Отправляем зашифрованные данные и ключ скрипту
// Здесь URLEncode нужно для экранирования '+'
URL := 'http://localhost/test.php?key=' + URLEncode(EncodedKey) + '&data=' + URLEncode(EncodedData);
if not InitWebTools then
RaiseLastOSError;
try
ReplyRAW := InternetGet(URL, [], []);
finally
DoneWebTools;
end;

// PHP-скрипт ничего не вернул?
if ReplyRAW = '' then
Abort;

// Опционально
SecureFree(URL);

// Удаляем ненужные данные
SecureFree(EncodedKey);
SecureFree(EncodedData);

finally
// На всякий случай (исключение) - чистим данные
SecureFree(SessionKey);
SecureFree(EncryptedData);
SecureFree(EncodedKey);
SecureFree(EncodedData);
end;

finally
// На всякий случай (исключение) - чистим данные
SecureFree(SessionKey);
SecureFree(SessionKeyRAW);
SecureFree(EncodedData);
SecureFree(EncodedKey);
SecureFree(JSONText);
SecureFree(JSONRAW);
end;

// Здесь у нас есть ReplyRAW - ответ от PHP-скрипта (байты)

try
// Конвертируем байты в текст
JSONText := UTF8ToString(ReplyRAW);

// Опционально
Finalize(ReplyRAW);

// Конвертируем текст JSON в объект JSON
JSON := JSONCreate(JSONText);

// Опционально
Finalize(JSONText);

// PHP-скрипт вернул ошибку?
if JSON.IndexOf('error') >= 0 then
raise Exception.Create(JSON['error']);

// Читаем лицензию и подпись
EncodedLicense := JSON['license'];
EncodedSignature := JSON['signature'];

// Текст в байты
License := Base64DecodeString(EncodedLicense);
Signature := Base64DecodeString(EncodedSignature);

// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try

// Проверяем цифровую подпись
if RSAVerify(RSAKey, Pointer(License), Length(License), Pointer(Signature), Length(Signature)) then
begin
// Цифровая подпись не нарушена
// Мы можем быть уверены, что License действительно пришла с нашего сервера

// Опционально
SecureFree(RSAKey);

// Просто для примера
// В реальной программе License была бы зашифрована
ShowMessage(UTF8ToString(License));
// Покажет 'This is just an example license'

end
else
ShowMessage('Not signed');

finally
// На всякий случай (исключение) - чистим данные
SecureFree(RSAKey);
end;

finally
// На всякий случай (исключение) - чистим данные
SecureFree(JSONText);
SecureFree(ReplyRAW);
SecureFree(EncodedSignature);
SecureFree(EncodedLicense);
SecureFree(Signature);
SecureFree(License);
end;
end;
<?php

// Эти функции необходимы, потому что MCrypt использует дополнение нулями вместо PKCS#5
// OpenSSL поддерживает PKCS#5, но не поддерживает Twofish

// Дополнение данных по PKCS#5
function pkcs5_pad($text, $blocksize = 16) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}

// Обрезка PKCS#5
function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}

// Секретный ключ
// Должен соответствовать открытому ключу, используемому в Delphi
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
$PrivateKey = <<<EOD
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u
...
X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK
N9AYlDckmF/X/1cZxWNyiltZZ542kezn
-----END PRIVATE KEY-----
EOD;

// Читаем переданные данные (зашифрованный сессионный ключ и зашифрованные данные)
$EncodedKey = $_GET['key'];
$EncodedData = $_GET['data'];

// Декодируем текст в байты
$EncryptedKey = base64_decode($EncodedKey);
$EncryptedData = base64_decode($EncodedData);

// Переводим little-endian (Windows/EurekaLog/WinCrypt) в big-endian (PHP/OpenSSL)
$EncryptedKey = strrev($EncryptedKey);

// Расшифровываем сессионный ключ
if (!openssl_private_decrypt($EncryptedKey, $Key, $PrivateKey)) {
echo('{ "error": ' . json_encode('openssl_private_decrypt: ' . openssl_error_string()) . ' }');
die();
}

// Расшифровываем текст (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$Data = pkcs5_unpad(mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedData, MCRYPT_MODE_ECB));

// Преобразуем строку JSON в объект JSON
$Data = json_decode($Data, true);

// Проверяем тип запроса
$Request = $Data['request'];
if (($Request['version'] < 1) || ($Request['version'] > 1)) {
echo('{ "error": "Unsupported request" }');
die();
}

// Запрашивают лицензию?
if ($Request['type'] == 'license') {

// Проверяем пользователя
$User = $Data['user'];
// Только для примера
$OK = (($User['login'] == 'input-from-edit1') && ($User['password'] == 'input-from-edit2'));
if ($OK) {

// Каким-то образом получаем лицензию для указанного пользователя
// В реальной программе это будут, вероятно, зашифрованные данные
$License = 'This is just an example license';

// Подписываем лицензию
openssl_sign($License, $Signature, $PrivateKey, OPENSSL_ALGO_SHA1);

// Переводим big-endian (PHP/OpenSSL) в little-endian (Windows/EurekaLog/WinCrypt)
$Signature = strrev($Signature);

// Кодируем байты в текст
$EncodedLicense = base64_encode($License);
$EncodedSignature = base64_encode($Signature);

// Возвращаем вызывающему лицензию с подписью
echo('{ "license": ' . json_encode($EncodedLicense) . ', "signature": ' . json_encode($EncodedSignature) . ' }');

} else {
echo('{ "error": "Access Denied" }');
die();
}
}

echo('{ "error": "Unsupported request" }');

Читать на сайте автора.