EurekaLog предлагает три модуля:
EEncoding
- содержит функции кодирования и преобразования данных;EHash
- содержит функции хэширования;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 - это неплохая хэш-функция общего назначения с равномерным распределением, удобна для использования в качестве ключа/индекса в БД
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, '\SOFTWARE\YourApp', 'SavedPassword', UserPassword); // Опционально SecureFree(UserPassword); finally // На всякий случай (выход по исключению) - затираем всё SecureFree(UserPassword); SecureFree(StoredPassword); SecureFree(ClearText); end; // ... // Далее у нас есть информация в зашифрованном виде, хранящаяся в реестре // Каждый раз, когда нам надо использовать конфиденциальную информацию, // мы должны прочитать её, расшифровать её, использовать, затем удалить расшифрованный вариант // ... // Читаем сохранённую зашифрованную конфиденциальную информацию UserPassword := RegKeyRead(HKEY_CURRENT_USER, '\SOFTWARE\YourApp', '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, '\SOFTWARE\YourApp', '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, '\SOFTWARE\YourApp', '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-----
.
Например, один и тот же публичный ключ экспортируется следующим образом:
rsBLOB
(заголовокPUBLICKEYBLOB/PRIVATEKEYBLOB
+ little-endian, ключ $25 $17 $B4 $A0 ... $96 $B9 $9C $E7 начинается с $15/21-го байта и до конца):
rsDER
(ASN.1 контейнер PKCS#1 + big-endian, т.е. тот же самый ключ $E7 $9C $B9 $96 ... $A0 $B4 $17 $25 начинается с 10-го байта и заканчивается на 6-м байте с конца):
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-----
Пример создания и экспорта нового ключа:
- На Delphi (PKCS#1):
uses EEncrypt; // для RSA функций procedure TForm1.Button1Click(Sender: TObject); var Key: TRSAKey; begin // Создали новую пару ключей (несколько секунд) Key := RSAGenKey; try // Экспорт обоих ключей во все форматы (для примера) RSASavePublicKey(Key, 'C:\Documents\public.blob', rsBLOB); RSASavePublicKey(Key, 'C:\Documents\public.der', rsDER); RSASavePublicKey(Key, 'C:\Documents\public.pem', rsPEM); RSASavePrivateKey(Key, 'C:\Documents\private.blob', rsBLOB); RSASavePrivateKey(Key, 'C:\Documents\private.der', rsDER); RSASavePrivateKey(Key, 'C:\Documents\private.pem', rsPEM); finally // Удалили ключи SecureFree(Key); end; end;
- На 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']);
- На 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). Здесь есть один подводный камень:
- 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 полей описывают различные компоненты ключа (модуль, экспоненту и т.д.). - 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"
, либо вы можете конвертировать закрытый ключ, а потом получить публичный ключ из закрытого.Суммируя вышесказанное, мы рекомендуем:
- Использовать утилиту EurekaLog Crypto Helper для создания пары RSA ключей:
- Запустить Пуск / Программы / EurekaLog / Tools / EurekaLog Crypto Helper
- Перейти на вкладку Keys
- Перейти на вкладку RSA
- Нажать кнопку Create New
- Сохранить закрытый и открытый ключи в файлы
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:\Documents\public.pem', rsPEM); RSASavePrivateKey(Key, 'C:\Documents\private.pem', rsPEM); finally // Удалили ключи SecureFree(Key); end; end;
- Импортировать ключи в Delphi (EurekaLog умеет загружать PKCS#1):
var RSAKey: TRSAKey; begin // Импорт PKCS#1 PEM RSAKey := RSALoadPrivateKey('C:\Documents\private.pem', rsPEM);
var RSAKey: TRSAKey; begin // Импорт PKCS#1 PEM RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM);
- Импортировать ключи в 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-----
.
- Скачать OpenSSL для Windows.
- Создать пару ключей, эти команды создадут два файла
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
- Импортировать ключи в Delphi (EurekaLog умеет загружать PKCS#8):
var RSAKey: TRSAKey; begin // Импорт PKCS#8 PEM RSAKey := RSALoadPrivateKey('C:\Documents\private.pem', rsPEM);
var RSAKey: TRSAKey; begin // Импорт PKCS#8 PEM RSAKey := RSALoadPublicKey('C:\Documents\public.pem', rsPEM);
- Импортировать ключи в 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
, которые используются аналогично функциям симметричного шифрования выше. Есть только несколько отличий:
- Поскольку асимметричное шифрование использует два разных ключа, оно используется в сценариях "отправитель-получатель" и не используется, когда шифрует и расшифровывает данные одно и то же лицо.
- Так как асимметричное шифрование работает очень медленно, то оно никогда не применяется к самим открытым данным. Вместо этого открытые данные шифруются любым симметричным шифром со случайным ключом (так называемый "сессионный ключ"), а затем симметричный ключ шифруется асимметричным шифрованием.
- Для шифрования используется открытый ключ, а для расшифровки - закрытый. Т.е. кто угодно может зашифровать данные открытым ключом получателя, при этом расшифровать данные сможет только получатель. Так и обеспечивается секретность.
Функции
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:\Documents\public.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:\Documents\EncryptedData.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:\Documents\Text.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:\Documents\EncryptedData.bin полностью готов // В нём лежит зашифрованный (случайный) симметричный ключ, а также зашифрованный файл C:\Documents\Text.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:\Documents\EncryptedData.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:\Documents\private.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:\Documents\Text2.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:\Documents\public.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:\Documents\public.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 очень не любит ошибок в менеджере памяти.
Настолько не любит, что, например, при повторном вызове FreeMem для уже освобождённого блока памяти врубает "режим паники" и прибивает всё приложение. Да, она показывает перед этим сообщение об ошибке, но это не даёт пользователю возможность хоть как-то продолжить работу и, например, сохранить свой документ на диск. Зачем так жестоко? Нельзя ли как-то отрубить этот функционал?
Да, я, как разработчик, осознаю, что что в результате повреждения памяти сохранённый пользователем документ может напрочь испортиться и готов нести ответственность за это, считая, что в большинстве случаев это меньшее зло, чем невозможность сохраниться. Но EurekaLog не предоставляет такой возможности.
См. Use safe mode to handle special exceptions.
Удалить> в результате повреждения памяти сохранённый пользователем документ может напрочь испортиться и готов нести ответственность за это
Там только не в этом дело. Этот функционал изначально создавался для другого. Ошибка с памятью может быть, если менеджеру памяти пришёл кирдык. Например, баг в коде затёр управляющие структуры. В этом случае попытка как-то обработать исключение приведёт лишь к повторной ошибке памяти. Цикл продолжится до "Приложение выполнило недопустимую операцию и будет закрыто". Результат: недовольные клиенты "EurekaLog не работает". А если учесть, что у нас проверки памяти включены по умолчанию, а в существующих проектах с памятью обычно швах, то и - "приложение вылетает после добавления в него EurekaLog". А вот эта функциональность позволяет EurekaLog даже в таких сложных случаях создать отчёт. Да, не всегда это возможно, но мы, по крайней мере, пытались.
Собственно, основная проблема - нет машины времени. Т.е. имея на руках исключение, нельзя сказать, можно ли его будет успешно обработать или нет. Поэтому приходится включать "безопасный режим" заранее для некоторых типов исключений - в основном это различные проблемы с памятью.
Огромное спасибо за ответ!
УдалитьОчень рад, что Вы работаете в команде EurekaLog (много лет не заглядывал в Ваш блог, и только сегодня про это узнал).