Работа с криптографией в 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" }');

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

Почему не зависящие от устройства растровые изображения перевёрнуты вверх ногами?

Это перевод Why are device-independent bitmaps upside down? Автор: Реймонд Чен.

Для всех, кто в первый раз знакомится с независимыми от устройства (device-independent) растровыми изображениями Windows,

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

EurekaLog и VirusTotal: непредвиденные последствия

К нам обратился человек, который сообщил о непредвиденном последствии загрузки программы с внедрённой EurekaLog на сервис VirusTotal.

Дело было так: клиент скомпилировал приложение с EurekaLog. В приложении была настроена отправка баг-отчётов по e-mail. Он загрузил скомпилированное приложение на сайт VirusTotal и получил результат проверки, что всё в порядке.

Пока всё достаточно типично. Странные дела начались на следующий день, когда клиент получил на e-mail отчёт от EurekaLog. Странность заключалась в том, что клиент не запускал своё приложение и не распространял его. А сам отчёт выглядел… необычно.

В частности, исполняемый файл был переименован в случайный набор букв, равно как и случайным набором оказались имя пользователя и компьютера. В списке модулей и процессов не оказалось ничего подозрительного, и вообще машина казалась «голой». Единственным выделяющимся аспектом была загруженная библиотека pancore.dll, которая создала один поток. Google подсказывает, что pancore.dll — это часть Oracle AutoVue, корпоративного решения для визуализации и просмотра CAD и подобных данных.

Ответ «загадки» пришёл позже. Вот как выглядели результаты анализа файла при первой проверке:

А вот что показывает сайт при повторной загрузке того же файла день спустя (после получения «загадочного» отчёта):

Как видим, результаты проверки изменились: в заголовок были добавлены «интересные» шаблоны поведения, а в полном отчёте появились новые вкладки с анализом поведения файла: какие файлы он открывает, какие URL посещает, какие ключи реестра изменяет, какие процессы запускает, и так далее.

Оказывается, VirusTotal запускает загруженные программы в нескольких виртуальных машинах / песочницах (т.н. multisandboxing), чтобы определить детали поведения. В частности, загруженный нами файл был проверен в C2AE (предположительно, это CAPE Sandbox), утилитой Sysinternals Sysmon, и собственной песочницей VirusTotal: Jujubox.

Такая возможность существует в VirusTotal с 2012 года, когда они использовали VirusTotal Cuckoofork — клон CuckooBox. В 2017 году VirusTotal запустили multi-sandbox, а в 2019 году Cuckoofork заменили на новый Jujubox Sandbox.

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

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

Добавление EurekaLog в программу вызывает EOutOfResources (Out of system resources)

К нам обратился человек, который пожаловался на то, что его приложение работало нормально, пока он не добавил в него EurekaLog. После включения в проекте EurekaLog стало появляться исключение Out of system resources. Исключение возбуждалось вспомогательной функцией OutOfResources из модуля Vcl.Graphics.

Стек вызова для исключения выглядел следующий образом:

  • Vcl.Graphics.OutOfResources
  • Vcl.Graphics.GDIError
  • Vcl.Graphics.GDICheck
  • Vcl.Graphics.TransparentStretchBlt
  • Vcl.Graphics.TBitmap.Draw
  • Vcl.Graphics.TCanvas.Draw
  • SomeComponent.TSomeDBGrid.DrawCell
  • Vcl.Grids.DrawCells
  • Vcl.Grids.TCustomGrid.Paint
  • Vcl.Controls.TCustomControl.PaintWindow
  • Vcl.Controls.TWinControl.PaintHandler
  • Vcl.Controls.TWinControl.WMPrintClient

Само исключение возбуждается такой функцией:

procedure OutOfResources;
begin
raise EOutOfResources.Create(SOutOfResources);
end;

Которая в свою очередь вызывается из:

procedure GDIError;
const
BufSize = 256;
var
ErrorCode: Integer;
Buf: array [Byte] of Char;
begin
ErrorCode := GetLastError;
if (ErrorCode <> 0) and (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nil, ErrorCode, LOCALE_USER_DEFAULT, Buf, BufSize, nil) <> 0) then
raise EOutOfResources.Create(Buf)
else
OutOfResources;
end;

function GDICheck(Value: THandle): THandle;
begin
if Value = 0 then
GDIError;
Result := Value;
end;

Заметьте, что такая реализация в VCL имеет проблему: вне зависимоси от ошибки возбуждается исключение класса EOutOfResources, даже если ошибка не равна ERROR_NOT_ENOUGH_MEMORY, ERROR_NO_SYSTEM_RESOURCES (или аналогичной). Логичнее было бы возбуждать что-то типа EInvalidGraphicOperation в общем случае и возбуждать EOutOfResources только для ошибок подобного типа.

Из полного баг-отчёта EurekaLog было видно, что показатели памяти и описателей находятся в разумной норме, т.е. проблема не в нехватке памяти. Откуда следует, что (вероятнее всего) GetLastError вернула 0. В самом деле, строка в TransparentStretchBlt, которая проваливает проверку GDICheck выглядит следующим образом:

MemBmp := GDICheck(CreateCompatibleBitmap(SrcDC, SrcW, SrcH));

Из документации видно, что функция CreateCompatibleBitmap не устанавливает значение GetLastError при неудаче.

Впрочем, у функции не так много причин завершиться неудачей: либо ей переданы неверные аргументы, либо ей нехватает памяти для создания bitmap. Заметьте, что нехватка памяти также возможна, если в SrcW и SrcH находится «мусор», который «слишком большой». Таким образом, хотя мы не знаем точную причину неудачи CreateCompatibleBitmap, но мы можем предположить, что проблема — в аргументах.

Значения SrcDC, SrcW и SrcH являются параметрами функции и приходят в неё из TBitmap.Draw:

TransparentStretchBlt
(ACanvas.FHandle, Left, Top, Right - Left, Bottom - Top,
Canvas.FHandle { SrcDC }, 0, 0,
FDIB.dsbm.bmWidth { SrcW }, FDIB.dsbm.bmHeight { SrcH },
MaskDC, 0, 0);

Где Canvas — это поле FCanvas bitmap-а, создаваемое по запросу, а FDIB — поле из FImage: TBitmapImage. Таким образом, все параметры (SrcDC, SrcW и SrcH) приходят в функцию TransparentStretchBlt из полей объекта класса TBitmap.

Следовательно, TBitmap, который пытается рисовать TSomeDBGrid.DrawCell, повреждён. Поскольку исключения не происходит без EurekaLog, но происходит с EurekaLog, то содержимое памяти TBitmap меняется при включении EurekaLog. Наиболее вероятное объяснение такого поведения: ошибка типа «use after free». Без отладочных инструментов в программе код может обратиться к уже удалённому TBitmap и «успешно» выполнить с ним операцию — поскольку память освобождённых объектов не удаляется физически, а лишь помечается как «свободная», без изменения её содежимого. При добавлении в программу EurekaLog её конфигурация по умолчанию включает проверки памяти, которые стирают память при её освобождении.

Проверить эту гипотезу можно изменив настройку «When memory is released» в положение «Do nothing». Если после этого исключение EOutOfResources пропадёт, то в коде имеется ошибка вида «use after free». Наиболее вероятна ошибка в коде SomeComponent, но есть небольшой ненулевой шанс, что клиент нашёл ошибку в VCL.

К сожалению, мы не получили ответа от клиента.

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

Пройди опрос — выиграй лицензию EurekaLog

Правила просты: вы помогаете нам улучшить EurekaLog, принимая участие в опросе. Заполнение анкеты займёт всего несколько минут. По окончании опроса будут выбраны 3 случайных участника.

  • Победители получат одну лицензию Single Developer Enterprise, если у них ещё нет лицензии.
  • Либо победитель получит бесплатное продление срока обслуживания существующей лицензии (на 2 года).

Пройти опрос можно тут.

Опрос завершится 31 января 2021 года. Победители будут объявлены 1 февраля 2021 года. Вы можете пройти опрос только один раз. Дубликаты будут удалены. С победителями свяжутся по электронной почте.

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

Даже в пустых приложениях есть баги

С нами связался человек, который утверждал, что нашёл баг в EurekaLog. Он обосновал это утверждение следующим образом: если создать новое приложение DataSnap и добавить в него EurekaLog, то приложение вылетит с Access Violation при выходе.

При расследовании выяснилось следующее:

  • В модуле данных ServerContainerUnit1 находится компонент сервера DSServer1;
  • Тот же модуль данных содержит два вспомогательных компонента: DSTCPServerTransport1 и DSServerClass1;
  • Оба компонента DSTCPServerTransport1 и DSServerClass1 указывают DSServer1 как «Server»;
  • При выходе приложения модуль данных ServerContainerUnit1 будет уничтожен;
  • Уничтожение модуля данных означает уничтожение всех компонентов в нём — включая DSTCPServerTransport1 и DSServerClass1.

    Вот где это происходит:

    • TDSServerClass.Destroy
    • TComponent.DestroyComponents
    • TDataModule.Destroy

    • TDSTCPServerTransport.Destroy
    • TComponent.DestroyComponents
    • TDataModule.Destroy

    При этом ни TDSServerClass.Destroy, ни TDSTCPServerTransport.Destroy не уведомляют сервер о том, что они уничтожаются. В результате DSServer1 продолжает хранить ссылку на (уже удалённые) DSTCPServerTransport1 и DSServerClass1.

  • Итак, после удаления DSTCPServerTransport1 и DSServerClass1 очистка модуля данных ServerContainerUnit1 продолжается;
  • Настаёт очередь удаления DSServer1. В частности, при этом DSServer1 пытается остановить все зарегистрированные в нём транспорты:
    • TDSCustomServer.StopTransports
    • TDSCustomServer.Stop
    • TDSServer.Stop
    • TDSServer.Destroy
    • TObject.Free
    • TComponent.DestroyComponents
    • TDataModule.Destroy

    что и вызовет Access Violation, поскольку объекты для этих транспортов уже удалены.

Как видим, это — баг в DataSnap, а не в EurekaLog. EurekaLog лишь помогла найти этот баг. Действительно, когда EurekaLog не подключена, то DSServer1 может «успешно» вызвать StopTransports, поскольку память уже удалённых объектов не будет изменена, и вызов методов уже удалённых объектов внутри StopTransports пройдёт «успешно».

Заметьте, что эта ошибка сидит в DataSnap уже целую вечность и её никто не исправляет — ровно потому, что в голом приложении нет средств для её обнаружения. Похожие проблемы есть и в пустом приложении Fire Monkey (FMX), а также во многих стандартных компонентах Delphi.

Конкретно эту ошибку можно обойти, если заставить DSServer1 удаляться первым:

procedure TServerContainer1.DataModuleDestroy(Sender: TObject);
begin
FreeAndNil(DSServer1);
end;

Другие ошибки аналогичного плана нужно исследовать отдельно.

Если же вы не можете исправить ошибку — вы всегда можете выключить проверки памяти в EurekaLog, хотя это и не рекомендуется.

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

Добавление EurekaLog в программу вызывает Integer Overflow?

К нам обратился человек, который пожаловался на то, что его приложение работало нормально, пока он не добавил в него EurekaLog. После включения в проекте EurekaLog стало появляться исключение Integer Overflow. Исключение происходило внутри функции _UStrCatN (функция конкатенации нескольких строк в RTL).

Функция _UStrCatN написана на ассемблере, но если вникнуть в смысл проверок, то получится что-то такое:

  DestLen := {... вычисляется длина результирующей строки ...};      
if DestLen < 0 then
_IntOver;

где _IntOver — это функция RTL, которая и вызывает исключение Integer Overflow.

Что происходит? Как длина строки может быть отрицательной? Это баг в EurekaLog?

Указанная проверка внутри _UStrCatN должна ограничить строки 2 Гб памяти: если результат сложения всех строк будет больше 2 Гб, то произойдёт переполнение, и длина станет отрицательной. Таким образом, Integer Overflow при сложении строк может возникать, если результат слишком большой.

Но при чём тут тогда EurekaLog? И как проверка может срабатывать, если мы складываем небольшие строки? (клиент подтвердил это логом)

Такое «ложно-положительное» срабатывание возможно, если вы проводите операцию с уже удалённой строкой.

Посмотрите на такой код:

var
Marker: String;

function ReadLine: String;
begin
// ...
Marker := { ... };
// ...
end;

begin
// ...
Data := Data + Marker + ReadLine;
// ...
end;

Видите ли вы проблему в этом коде?

Чтобы понять проблему, нужно знать как выполняется строка «Data := Data + Marker + ReadLine;«. На псевдо-коде это выглядит как-то так:

Param0 := Pointer(Data);
Param1 := Pointer(Marker);
Param2 := Pointer(ReadLine);
_UStrCatN(Data, [Param0, Param1, Param2]);

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

Вот вам и проблема: оператор сохраняет указатель на строку Marker, но строка Marker меняется внутри функции ReadLine. Это означает, что сохранённый указатель будет указывать на старую строку. Таким образом, на вход функции _UStrCatN попадёт уже удалённая строка.

Заметьте, что без EurekaLog в проекте этот баг не является «проблемой». Действительно, удалённая память просто помечается как свободная, но её содержимое не очищается. Это значит, что _UStrCatN успешно проведёт конкатенацию с уже удалённой строкой. И результат операции, скорее всего, будет корректным. Т.е. баг в коде есть, но его совершенно не видно, поскольку программа функционирует полностью правильно.

Ситуация меняется в корне, если в проект добавляется EurekaLog (или любой другой инструмент для отладки проблем с памятью). По умолчанию в EurekaLog включены проверки памяти. Это означает, что удалённая память будет очищена. Как правило, это делается шаблоном вроде DEADBEEF. Заметьте, что Integer представление DEADBEEF — отрицательно (равно -559038737). Т.е. прибавление к этому числу длин нескольких небольших строк также даст отрицательное число.

Иными словами, если в проект добавлена EurekaLog, то операция с уже удалённой строкой больше не будет успешной. Ранее скрытый баг теперь виден.

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

Почему в выводе echo командной строки с перенаправлением появляется 1? Кто вставляет эти единицы?

Это перевод Why does my command line redirection echo with an extra 1? Who’s inserting these rogue 1s everywhere? Автор: Реймонд Чен.

Если вы оставите включённым

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

Добавление EurekaLog в программу вызывает Access Violation?

К нам обратился человек, который пожаловался на то, что его приложение работало нормально, пока он не добавил в него EurekaLog. После включения в проекте EurekaLog стало появляться исключение AccessViolation с текстом Access violation at address 00E15025 in module ‘Project.exe’. Read of address 83EC8B69 и таким стеком:

  • Contoso.pas TContosoEventMessage.BasePerform
  • Vcl.Forms.pas TApplication.WndProc
  • System.Classes.pas StdWndProc
  • Vcl.Forms.pas TApplication.HandleMessage
  • Vcl.Forms.pas TApplication.Run
  • Project.dpr Initialization

(все имена заменены; Contoso — это некая известная библиотека для Delphi).

Воспроизводимый пример человек предоставить не захотел, но, к счастью, у нас был доступ к исходному коду библиотеки.

Адрес в сообщении об ошибке (83EC8B69 — «случайный») и короткий стек могут указывать на проблему «управление ушло по мусорному указателю и вылетело в случайном месте». К сожалению, в отчёте не были включены секции CPU и ассемблера (отключены в настройках). В этом случае диагностика по исходному коду была бы невозможной. Признаться, я сначала хотел так и ответить, дав рекомендацию по усилению контроля за памятью.

Но вот именно такой стек возможен также в случае, если BasePerform вызывается главным потоком как процедура, поставленная в очередь для Synchronize. И действительно — поиск по исходникам выдал такой код:

function TContosoEventMessage.Perform(AThread: TContosoThread): Boolean;
begin
FMsgThread := AThread as TContosoEventThread;
if FMsgThread.Active then begin
if FMsgThread.Options.Synchronize then
TContosoThread.Queue(nil, BasePerform)
else
BasePerform;
end;
Result := True;
end;

Т.е. у класса сообщения есть функция для обработки этого сообщения. И если в опциях чего-то там указан флаг Synchronize, то непосредственно обработка сообщения (BasePerform) будет выполнена не в текущем потоке, а будет отправлена на выполнение в главный поток — через TThread.Queue.

И да, в отчёте также был виден этот фоновый поток от Contoso, который мог бы выполнить такой код.

Сам метод BasePerform является просто обёрткой вокруг InternalHandle:

procedure TContosoEventMessage.BasePerform;
begin
FMsgThread.InternalHandle(Self);
end;

И отчёт EurekaLog показывает, что вылет произошёл на первой же строчке.

Что мы имеем?

  1. Фоновый поток поставил в очередь метод, заполнив поле FMsgThread.
  2. Главный поток попробовал выполнить этот метод и вылетел при попытке прочитать поле FMsgThread.

Вторая строка является предположением, поскольку, как я уже сказал, ассемблера в отчёте не было. Но гипотеза вполне себе, поскольку если иначе вылет был бы внутри InternalHandle.

Т.е. вроде как налицо проблема с памятью. И, кстати говоря, это самая частая «проблема» проектов, которые включают EurekaLog. В самом деле, посмотрите на такой код:

procedure TForm1.Button1Click(Sender: TObject);
var
List: TList;
begin
List := TList.Create;
List.Free;

List.Clear; // - использование объекта после удаления
end;

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

Добавление EurekaLog в программу меняет ситуацию на корню, поскольку EurekaLog по умолчанию включает проверки памяти и активно борется с указанными багами.

Что-ж, посмотрим может ли у нас сейчас быть такая ситуация. Для этого нужно понять где освобождается память для FMsgThread. Поскольку он приходит из параметра метода — посмотрим, кто вызывает метод BasePerform. Поиск по исходникам подсказал такой код (поиск показал несколько мест, но конкретное место было выбрано исходя из состояние фонового потока из отчёта):

procedure TContosoThread.Execute;
// ...
begin
// ...
for i := 0 to oReceivedMsgs.Count - 1 do
begin
oMsg := TContosoThreadMsgBase(oReceivedMsgs[i]);
try
if not oMsg.Perform(Self) then
Break;
finally
oMsg.Destroy;
end;
// ...
end;

Оп, а вот он и баг. Сценарий происходящего:

  1. Фоновый поток перебирает входящие сообщения.
  2. Каждое сообщение обрабатывается (Perform).
  3. Какое-то сообщение указывает, что его нужно синхронизировать в главный поток.
  4. Фоновый поток ставит сообщение в очередь (Queue).
  5. Фоновый поток удаляет сообщение (Destroy), но указатель на сообщение всё ещё сидит в очереди.
  6. Главный поток приступает к обработке запланированных сообщений (BasePerform).
  7. Обработка сообщения вызывает Access Violation, потому что сообщение уже удалено.

Т.е. баг тут либо в управлении памятью, либо в синхронизации потоков. Как возникла такая ошибка? Очевидно, изначально были написаны базовые классы, которые предполагали линейное выполнение: «взяли сообщение — обработали — освободили». Потом был написан особый класс-наследник, который переопределил поведение «обработать» на «поставить в очередь и выйти». А базовый класс (с циклом) про это не в курсе. Что и привело к багу.

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

Я подозреваю, что исправление проблемы могло бы быть в замене Queue на Synchronize.

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

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

Ответ на задачку №26

Ответ на задачку №26.

Не сказать, что было много вариантов ответов, но один ответ был достаточно близок к истине. Да, задачка была сильно неочевидная.

Напомню код функции:

function InitDeflate(const ACompressionLevel: Byte): TZStreamRec;
var
Code: Integer;
begin
FillChar(Result, SizeOf(Result), 0);
Code := System.ZLib.deflateInit_(Result, ACompressionLevel, zlib_version, SizeOf(TZStreamRec)));

// ... далее идёт анализ Code
// в данном случае Code = Z_OK
end;

Запись TZStreamRec выглядит так:

type
TZStreamRec = record
next_in: PByte; // следующий входной байт
avail_in: Cardinal; // число байт в next_in
total_in: LongWord; // сколько уже байт прочитали
next_out: PByte; // сюда будет записан следующий выходной байт
avail_out: Cardinal; // сколько осталось свободного места в next_out
total_out: LongWord; // сколько всего байт записали
msg: PAnsiChar; // последнее сообщение об ошибке, nil если ошибки не было
state: Pointer; // недокументировано
zalloc: alloc_func; // для выделения памяти для state
zfree: free_func; // для освобождения памяти state
opaque: Pointer; // "непрозрачный" параметр для zalloc и zfree
data_type: Integer; // возможный тип данных (двоичные/текст) для сжатия,
// или состояние процесса для распаковки
adler: LongWord; // контрольная сумма распакованных данных
reserved: LongWord; // зарезервированно, должно быть 0
end;

Короче говоря, это достаточно простая функция-обёртка. И обычно она будет работать без проблем. Вернее, она-то всегда будет работать нормально — в том смысле, что она всегда будет возвращать Z_OK. Но иногда любая попытка воспользоваться результатом функции (например, в вызове deflate) вернёт ошибку Z_STREAM_ERROR в первом же вызове!

Проблема кроется в недокументированном поле state. Если мы откроем исходники ZLib, то увидим такой код (сокращено):

typedef struct internal_state {
z_streamp strm; /* указатель на TZStreamRec, которому принадлежит это состояние */
int status; /* как следует из названия */
/* ... */

Иными словами, поле state — это указатель на запись, в поле которой записывается указатель на родительскую запись (TZStreamRec). Что-то вроде Owner-а у классов. Проблема в том, что записи — это не классы. Их экземпляры передаются по значению, а не по ссылке (как объекты). Это значит, что если мы присвоим TZStreamRec другой переменной — это скопирует все поля записи в новую область памяти (переменную). У этой переменной будет свой собственный адрес, не совпадающий со старым. В то же время операция присваивания ничего не знает про поле state, поэтому оно не будет изменено и продолжит указывать на старую запись. Вызов deflate увидит, что указатель strm не указывает на валидную запись TZStreamRec и вернёт Z_STREAM_ERROR.

Но где же проблема? Действительно, при вызове функции компилятор передаст в неё указатель на переменную (обычно — стек), внутри функции этот указатель будет передан сначала в FillChar, а затем и в deflateInit. Поскольку всюду запись передаётся по указателю — проблем быть не должно:

; ZLIBStream := InitDeflate(9);
lea ecx,[ebp-$4d]
mov al,$09
call InitDeflate

Но иногда компилятор может решить сделать такое:

; ZLIBStream := InitDeflate(9);
mov rcx,rbp
lea rdx,[rbp+$48]
mov r8b,$09
call InitDeflate
lea rdi,[rbp+$000000b0]
lea rsi,[rbp+$48]
mov ecx,$0000000c
rep movsq

Т.е. на псевдокоде:

Tmp := InitDeflate(9);
ZStream := Tmp;

Вот вам и копирование. Вот вам и проблема.

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

Чья же здесь ошибка? Я считаю, что программиста. Мы неявно использовали допущение о внутренней реализации. Это примерно как задачка №13.

Как это можно исправить? Передавать ссылку явно:

procedure InitDeflate(const ACompressionLevel: Byte; out Result: TZStreamRec);
var
Code: Integer;
begin
FillChar(Result, SizeOf(Result), 0);
Code := System.ZLib.deflateInit_(Result, ACompressionLevel, zlib_version, SizeOf(TZStreamRec)));

// ...
end;

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

P.S. Я ругался на «нововведения» ZLib, потому что «ранее этот код работал» (со старой ZLib) и упоминания о такой детали реализации или же требовании я в документации не нашёл. Но пока писал ответ, мне пришло в голову, что, возможно, дело в компиляторе Delphi. Возможно, где-то когда-то что-то поменяли. И ранее там не было скрытой временной переменной, а реализация в ZLib не менялась. Возможно. Я не знаю.

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