Delphi 10.5 отменяется – ждем Delphi 11
Компания Embarcadero вместо запланированной в Delphi, C++Builder и RAD Studio roadmap 2020/2021 версии 10.5 выпустит 11-ю версию. Это связано с нумерацией версий операционных систем, на
Delphi, C++Builder и RAD Studio 10.5 Beta
[Android Service] Пример Started сервиса
Пример очень простой и призван показать самые основы работы с сервисом.Создание основы для дальнейшей разработки:1. Создаём "Local сервис" с помощью "Мастера создания сервиса"2. Переименовываем файл
[Android Service] Сервисы в Android
Поговорим об Android сервисах. Для тех, кто уже знаком с сервисами, чего-то нового вряд ли тут найдёте. Хочу сразу предупредить, я не эксперт в этой
[Android Service] Добавление/удаление сервиса в FMX
Добавление сервиса в проект приложения.Примечание 1. Перед добавлением сделайте сборку (Build для Android 32-bit и Android 64-bit) проекта сервиса, чтобы сгенерировать двоичные файлы.1. Откройте проект
Работа с криптографией в EurekaLog
EurekaLog — это трейсер исключений, т.е. инструмент, который устанавливает хуки и перехватывает возбуждение исключений, позволяя создать отчёт по необработанным исключениям. Тем не менее, в ней есть различного рода дополнительная функциональность, которую вы можете использовать в своих программах. И одной из таких возможностей являются функции криптографии.
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 — это неплохая хэш-функция общего назначения с равномерным распределением, удобна для использования в качестве ключа/индекса в БД
Все функции хэширования имеют имя ИмяХэшаHash (например, MD5Hash()
), возвращают результат типа TИмяХэшаHash
(например, TSHA1Hash
), а на вход принимают RawByteString
, либо пару указатель+данные.
Также для некоторых хэшей в EurekaLog есть реализация алгоритма HMAC, который может использоваться для различных целей. Один из способов использования — аутентификация пользователя путём комбинации соли (salt) и пароля для получения хэша через HMAC. Функции для HMAC имеют вид ИмяХэшаHMAC (например, MD5HMAC()
) и принимают пароль и соль.
Вот несколько примеров:
1. Считаем хэш строки:
uses
EEncoding, // для HexEncodeToString
EHash; // для MD5Hash
procedure TForm1.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
UTF8Str: UTF8String; // Байтовое представление
Hash: TMD5Hash; // Хэш
begin
// Определим исходные данные
S := 'Привет';
// Определим точное представление в байтах
// В данном случае - UTF-8
UTF8Str := UTF8Encode(S);
// (можно просто UTF8Str := S; в Delphi 2009 и выше)
// Вычислим хэш
Hash := MD5Hash(UTF8Str);
// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет '8A669E9418750C81AB90AE159A8EC410'
end;
2. Считаем хэш файла:
uses
EEncoding, // для HexEncodeToString
EHash, // для SHA256Hash
ECompatibility; // для FileToString
procedure TForm1.Button1Click(Sender: TObject);
var
Content: RawByteString; // Исходные байты файла
Hash: TSHA256Hash; // Хэш
begin
// Загружаем файл в двоичную строку
Content := FileToString(ParamStr(0));
// В Content будет 'MZP'#0#2#0#0#0...
// Вычислим хэш
Hash := SHA256Hash(Content);
Finalize(Content); // не обязательно
// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет что-то вроде 'FCF52FDC753E3797FE5EE4B5A7680E656D044D6BF7D97C408F0F7874492E43C2'
end;
3. Считаем хэш строки в произвольной кодировке:
uses
EEncoding, // для HexEncodeToString (и TEncoding для старых Delphi)
EHash; // для CRC32Hash
procedure TForm21.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
Encoding: TEncoding; // Кодировка для конвертирования в байты
Content: TBytes; // Байтовое представление
Hash: TCRC32Hash; // Хэш
begin
// Определим исходные данные
S := 'Привет';
// Получаем любую кодировку
Encoding := TEncoding.GetEncoding(866);
// Также можно:
// Encoding := TEncoding.UTF8;
// Encoding := TEncoding.Unicode;
// Encoding := TEncoding.ANSI;
try
// Конвертируем строку в байты
Content := Encoding.GetBytes(S);
finally
FreeAndNil(Encoding);
end;
// Вычислим хэш
Hash := CRC32Hash(Pointer(Content), Length(Content));
Finalize(Content); // не обязательно
// Покажем хэш человеку
Label1.Caption := HexEncodeToString(@Hash, SizeOf(Hash));
// Покажет '6DB3A7B9'
// Также можно IntToStr(Hash) - что даст 3114775405
end;
4. Обмениваемся хэшем с PHP:
uses
EEncoding, // для HexEncodeToString
EHash, // для MD5Hash
ECore; // для ShellExec
procedure TForm1.Button1Click(Sender: TObject);
var
S: String; // Исходная строка
UTF8Str: UTF8String; // Байтовое представление строки
Hash: TMD5Hash; // Хэш
HashStr: String; // Текстовое представление хэша
begin
// Определим исходные данные
S := 'Привет';
// Определим точное представление в байтах
// В данном случае - UTF-8
UTF8Str := UTF8Encode(S);
// (можно просто UTF8Str := S; в Delphi 2009 и выше)
// Вычислим хэш
Hash := MD5Hash(UTF8Str);
// Преобразуем двоичное представление в текст
HashStr := HexEncodeToString(@Hash, SizeOf(Hash));
// Хранит '8A669E9418750C81AB90AE159A8EC410'
// Передадим хэш в скрипт PHP
ShellExec(Format('http://localhost/test.php?hash=%s', [HashStr]));
end;
<?php
// Исходная строка (хранится в UTF-8)
$Source = 'Привет';
// Вычислим хэш исходной строки
// (функция вернёт строковое представление хэша)
$Hash = md5($Source);
// Прочитаем аргумент скрипта
$HashArg = $_GET['hash'];
// Проверим, совпадает ли хэш, сравнивая строковые представления
if (strtolower($Hash) == strtolower($HashArg)) {
// также можно использовать (начиная с PHP 5.6):
// if (hash_equals($HashArg, $Hash)) {
echo('OK'); // должны попасть сюда,
// т.е. исходные строки совпадают
// 'Привет' в Delphi = 'Привет' в PHP
} else {
echo('FAIL');
}
5. Хранение учётных данных в БД:
uses
EHash, // для SHA256HMAC
EEncrypt, // для InitSalt
EEncoding; // для RAWToString и HexEncodeToString/HexDecodeFromString
procedure TForm1.Button1Click(Sender: TObject);
var
UserName: String; // Пользовательский логин
UserPassword: String; // Пользовательский пароль
UserPasswordRAW: RawByteString; // Двоичное представление пароля
Salt: TSalt; // Соль (salt)
SaltStr: String; // Строковое представление соли
Hash: TSHA256Hash; // Хэш пароля
Hash2: TSHA256Hash; // Сохранённый хэш пароля
HashStr: String; // Строковое представление хэша
begin
// Шаг 1. Регистрация аккаунта
// Каким-то образом получаем логин и пароль
UserName := InputBox('Sign in', 'Enter the login:', '');
UserPassword := InputBox('Sign in', 'Enter the password:', '');
// Генерируем случайные байты для использования их в качестве соли
Salt := InitSalt;
// Преобразуем пароль в двоичное представление
UserPasswordRAW := UTF8Encode(UserPassword);
// Вычисляем хэш от соли и пароля через HMAC
Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW));
// Кодируем двоичные данные в текст
SaltStr := HexEncodeToString(@Salt, SizeOf(Salt));
HashStr := HexEncodeToString(@Hash, SizeOf(Hash));
// Вставляем в БД новую запись
// Это псевдо-код
InsertIntoDBTable('users', ['login', 'salt', 'password'], [UserName, SaltStr, HashStr]);
// Здесь:
// 'users' - имя таблицы
// 'login' - строковое поле произвольной длины
// 'salt' - строковое поле в 32 символа или двоичное поле в 16 байт
// 'password' - строковое поле в 64 символа или двоичное поле в 32 байта
// Шаг 2. Проверка логина
// Каким-то образом получаем логин и пароль
UserName := InputBox('Log in', 'Enter the login:', '');
UserPassword := InputBox('Log in', 'Enter the password:', '');
// Ищем в БД запись пользователя с введённым логином
// Это псевдо код, просто для примера
// В реальном коде нужно использовать параметры БД
Query := Format('SELECT salt, password FROM users WHERE login = ''%s'' LIMIT 1', [UserName]);
Values := DBQuery(Query);
// Если записи нет - введён неверный логин
if Length(Values) = 0 then
begin
ShowMessage('Invalid login');
Exit;
end;
// Преобразуем соль и хэш из текста в двоичные данные
SaltStr := Values[0]; // поле 'salt' из SELECT
HashStr := Values[1]; // поле 'password' из SELECT
Assert(HexCalcDecodedSize(Length(SaltStr)) = SizeOf(Salt));
HexDecodeFromString(SaltStr, @Salt);
Assert(HexCalcDecodedSize(Length(HashStr)) = SizeOf(Hash2));
HexDecodeFromString(HashStr, @Hash2);
// Повторим вычисления хэша, как и выше
UserPasswordRAW := UTF8Encode(UserPassword);
Hash := SHA256HMAC(@Salt, SizeOf(Salt), Pointer(UserPasswordRAW), Length(UserPasswordRAW));
// Здесь у нас есть:
// Hash - хэш введённого пароля
// Hash2 - хэш сохранённого пароля
// Если два хэша равны, то равны и пароли
// Проверяем, что пароль был введён верно, сравнивая хэши
if CompareMem(@Hash, @Hash2, SizeOf(Hash)) then
ShowMessage('OK')
else
ShowMessage('Invalid password');
end;
<?php
// Шаг 1. Регистрация аккаунта
// Каким-то образом получаем логин и пароль
$UserName = $_GET['login'];
$UserPassword = $_GET['password'];
// Генерируем случайные байты для использования их в качестве соли
$Salt = random_bytes(16);
// Вычисляем хэш от соли и пароля через HMAC
$HashStr = hash_hmac('sha256', $Salt, $UserPassword);
// Кодируем двоичные данные в текст
$SaltStr = bin2hex($Salt);
// Вставляем в БД новую запись
// Это псевдо-код
InsertIntoDBTable('users', ['login', 'salt', 'password'], [$UserName, $SaltStr, $HashStr]);
// Здесь:
// 'users' - имя таблицы
// 'login' - строковое поле произвольной длины
// 'salt' - строковое поле в 32 символа или двоичное поле в 16 байт
// 'password' - строковое поле в 64 символа или двоичное поле в 32 байта
// Шаг 2. Проверка логина
// Каким-то образом получаем логин и пароль
$UserName = $_GET['login'];
$UserPassword = $_GET['password'];
// Ищем в БД запись пользователя с введённым логином
// Это псевдо код, просто для примера
// В реальном коде нужно использовать параметры БД
$Query = 'SELECT salt, password FROM users WHERE login = '' . $UserName . '' LIMIT 1';
$Values = DBQuery($Query);
// Если записи нет - введён неверный логин
if (empty($Values)) {
echo('Invalid login');
die;
}
// Преобразуем соль и хэш из текста в двоичные данные
$SaltStr = Values['salt']; // поле 'salt' из SELECT
$HashStr2 = Values['password']; // поле 'password' из SELECT
$Salt = hex2bin($SaltStr);
// Повторим вычисления хэша, как и выше
$HashStr = hash_hmac('sha256', $Salt, $UserPassword);
// Здесь у нас есть:
// $HashStr - строковое представление хэша введённого пароля
// $HashStr2 - строковое представление хэша сохранённого пароля
// Если два хэша равны, то равны и пароли
// Проверяем, что пароль был введён верно, сравнивая хэши
if (strtolower($HashStr) == strtolower($HashStr2)) {
// также можно использовать (начиная с PHP 5.6):
// if (hash_equals($HashStr2, $HashStr)) {
echo('OK'); // должны попасть сюда,
// т.е. пароли совпали
} else {
echo('Invalid password');
}
Шифрование
В EurekaLog есть такие функции для шифрования:
- Внутрипроцессное шифрование (например, для защиты паролей)
- Межпроцессное шифрование (для защиты внешних данных в рамках пользовательского аккаунта или всего компьютера)
- TEA
- Twofish
- RSA
Как и функции хэширования, функции шифрования принимают на вход указатель на данные + их размер, либо RawByteString
. Но поскольку, в отличие от функций хэширования, функциям шифрования также нужно возвращать данные произвольного размера — вы также можете использовать запись TEncryptBuffer
, которая просто объединяет указатель + размер в один параметр.
Внутрипроцессное шифрование
Иногда в приложении необходимо оперировать «секретной» информацией. Чтобы уменьшить риски утечки этой информации, необходимо хранить её в зашифрованном виде. Подробнее о такой практике можно почитать в MSDN или в (крайне рекомендую) книге. EurekaLog предоставляет функции для оперирования конфиденциальной информацией:
Функция MemProtect
зашифровывает указанный блок памяти в процессе так, что расшифровать его можно только из этого же процесса. Функция MemUnprotect
расшифровывает блок памяти, зашифрованный MemProtect
. Функцию же SecureFree
можно использовать для удаления почти чего угодно. Функция затрёт содержимое перед освобождением памяти.
Например:
uses
EEncrypt; // для MemProtect/MemUnprotect и SecureFree
procedure TForm1.Button1Click(Sender: TObject);
var
UserPassword: String;
StoredPassword: TEncryptBuffer;
ClearText: TEncryptBuffer;
begin
// Обнуляем все буферы
FillChar(StoredPassword, SizeOf(StoredPassword), 0);
FillChar(ClearText, SizeOf(ClearText), 0);
// Каким-то образом получаем конфиденциальную информацию
UserPassword := InputBox('Query', 'Enter the password:', '');
try
// Шифруем информацию
ClearText.pbData := Pointer(UserPassword);
ClearText.cbData := Length(UserPassword) * SizeOf(Char);
MemProtect(ClearText, StoredPassword);
finally
// Затираем оригинал в открытом виде
SecureFree(UserPassword);
// Нет необходимости удалять ClearText,
// поскольку мы не выделяли для него память
end;
// ...
// Далее у нас есть StoredPassword - информация в зашифрованном виде
// Каждый раз, когда нам надо использовать конфиденциальную информацию,
// мы должны расшифровать её, использовать, затем удалить расшифрованный вариант
// ...
// Расшифровываем информацию
MemUnprotect(StoredPassword, ClearText);
try
// Как-то используем конфиденциальную информацию
Hash := MD5Hash(ClearText.pbData, ClearText.cbData);
finally
// Затираем оригинал в открытом виде
SecureFree(ClearText);
// Также можно удалить производную информацию
SecureFree(Hash);
end;
// ...
// В конце работы - удаляем зашифрованную информацию
SecureFree(StoredPassword);
end;
Межпроцессное шифрование
Иногда возникает необходимость хранить конфиденциальную информацию. Например, опция «Запомнить меня» может сохранить пароль аккаунта в реестре. В этом случае функции MemProtect
/MemUnprotect
не помогут, поскольку они не работают между процессами (а перезапуск программы и чтение сохранённых данных — это новый процесс). Для таких случаев в EurekaLog есть похожие функции DataProtect
и DataUnprotect
. Например:
uses
EEncrypt, // для DataProtect/DataUnprotect и SecureFree
EConfig, // для RegKeyWrite/RegKeyRead
EEncoding; // для Base64EncodeString/Base64DecodeString
procedure TForm1.Button1Click(Sender: TObject);
var
UserPassword: String;
StoredPassword: RawByteString;
ClearText: RawByteString;
begin
// Каким-то образом получаем конфиденциальную информацию
UserPassword := InputBox('Query', 'Enter the password:', '');
try
// Конвертируем в RawByteString для удобства
ClearText := UTF8Encode(UserPassword);
// Затираем оригинал в открытом виде
SecureFree(UserPassword);
// Шифруем информацию
StoredPassword := DataProtect(ClearText);
// или:
// StoredPassword := DataProtect(ClearText, True);
// если вы хотите использовать HKEY_LOCAL_MACHINE ниже
// Затираем оригинал в открытом виде
SecureFree(ClearText);
// Кодируем зашифрованные двоичные данные в строку (текст)
UserPassword := Base64EncodeString(StoredPassword);
// Сохраняем зашифрованную конфиденциальную информацию в реестре
RegKeyWrite(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'SavedPassword', UserPassword);
// Опционально
SecureFree(UserPassword);
finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(UserPassword);
SecureFree(StoredPassword);
SecureFree(ClearText);
end;
// ...
// Далее у нас есть информация в зашифрованном виде, хранящаяся в реестре
// Каждый раз, когда нам надо использовать конфиденциальную информацию,
// мы должны прочитать её, расшифровать её, использовать, затем удалить расшифрованный вариант
// ...
// Читаем сохранённую зашифрованную конфиденциальную информацию
UserPassword := RegKeyRead(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'SavedPassword', '');
try
// Декодируем строку (текст) в зашифрованные двоичные данные
StoredPassword := Base64DecodeString(UserPassword);
// Опционально - удаляем прочитанные зашифрованные данные
SecureFree(UserPassword);
// Расшифровываем информацию
ClearText := DataUnprotect(StoredPassword);
// Опционально - удаляем прочитанные зашифрованные данные
SecureFree(StoredPassword);
// Поскольку ранее для удобства мы кодировали данные - нужно сделать обратную конвертацию
UserPassword := UTF8ToString(ClearText);
// Затираем оригинал в открытом виде
SecureFree(ClearText);
// Как-то используем конфиденциальную информацию
Hash := MD5Hash(Pointer(UserPassword), Length(UserPassword) * SizeOf(Char));
// Затираем оригинал в открытом виде
SecureFree(UserPassword);
finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(UserPassword);
SecureFree(StoredPassword);
SecureFree(ClearText);
end;
end;
Симметричное шифрование
Для симметричного шифрования EurekaLog поддерживает TEA и Twofish. Оба алгоритма шифрования не патентованы и могут быть свободно использованы в любой программе. TEA используется в широком спектре аппаратного обеспечения благодаря крайне низким требованиям к памяти и простоте реализации. Twofish — надёжный алгоритм симметричного шифрования общего назначения.
Обратите внимание, что зашифрованные данные могут быть больше по размеру чем исходные — поскольку симметричные алгоритмы шифрования часто оперируют блоками данных. Отличительной особенностью TEA является то, что зашифрованные данные будут равны по размеру исходным данным, поэтому у функций шифрования/расшифровки TEA есть перегруженный вариант для операций in-place, т.е. без перераспределения памяти. Для алгоритма Twofish размер данных должен быть кратен размеру блока (16 байт) — в противном случае данные будут дополнены (padding) алгоритмом PKCS#5 до минимально необходимого размера.
Для обоих алгоритмов EurekaLog предоставляет algEncrypt/algDecrypt функции, которые выглядят и работают одинаково. На вход функции принимают ключ шифрования и исходные данные. Разница есть только в том, что варианты для Twofish позволяют указать необязательный вектор инициализации (IV — initialization vector). Вектор инициализации — это просто случайный набор байт (который можно получить вызовом функции TwofishInitIV
), который служит для того, чтобы два одинаковых блока данных шифровались бы по-разному, некий аналог соли (salt). Вектор инициализации не должен быть секретным и он передаётся (если есть) вместе с зашифрованными данными.
Оба алгоритма используют двоичные ключи для шифрования и расшифровки. Поскольку алгоритмы симметричные, то ключ для шифрования обязан совпадать с ключём для дешифрования. Разумеется, ключи для TEA и Twofish имеют разный (но фиксированный) размер в байтах. Как правило, ключи шифрования — не случайны, а получаются из паролей, вводимых пользователем. Чтобы преобразовать произвольный пароль в ключ фиксированной длины — используются функции algDeriveKey, на вход которым передаётся блок данных произвольного размера. «Вывод» ключа из пароля получается простым вызовом функции хэша от пароля с подходящим по размеру результатом. Например, для TEA это будет MD5, для Twofish — SHA-256. У derive-функций также есть перегруженный вариант, принимающий пароль в виде строки с опциональной солью (salt). В этом случае пароль конвертируется в UTF-8 представление, и хэш берётся от строки ‘соль’ + ‘UTF-8 пароль’. Кроме того, есть ещё вариант функции algDeriveKeyHMAC, который использует алгоритм HMAC для комбинации соли и пароля. В целом, если вы планируете использовать соль, то мы рекомендуем использовать функции algDeriveKeyHMAC.
Если ключи шифрования получаются как-то иначе (не из пароля) — вы также можете обмениваться ими напрямую, не «выводя» их из пароля. Просто обращайтесь с ключём, как с записью/набором байт фиксированного размера. Единственная тонкость — EurekaLog использует оптимизацию с Twofish: ключ используется не напрямую, а сначала преобразовывается в промежуточный вариант, что позволяет оптимизировать операции шифрования и расшифровки. Исходный ключ называется TTwofishRAWKey
, а оптимизированный вариант — TTwofishKey
.
Например:
1. Шифрование и расшифровка данных по паролю:
uses
EEncrypt; // для InitSalt, TEADeriveKey, TEAEncrypt/TEADecrypt, SecureFree
procedure TForm1.Button1Click(Sender: TObject);
var
Salt: TSalt; // Соль для преобразования пароля в ключ
Key: TTEAKey; // Ключ для шифрования и дешифрования
Source: String; // Исходный текст
SourceBytes: RawByteString; // Байтовое представление исходного текста
EncryptedBytes: RawByteString; // Зашифрованные исходные данные
begin
// Обнуляем все буферы
FillChar(Salt, SizeOf(Salt), 0);
FillChar(Key, SizeOf(Key), 0);
// Шаг 1: шифрование
try
// Определили исходные данные
Source := 'Привет';
// Определили байтовое представление исходных данных
// В данном случае - UTF-8
SourceBytes := UTF8Encode(Source);
// Исходные данные больше не нужны - затираем
SecureFree(Source);
// Создали случайный набор байтов для соли
Salt := InitSalt;
// Получили ключ из пароля
Key := TEADeriveKeyHMAC('супер секретный пароль', Salt);
// Зашифровали открытые данные
EncryptedBytes := TEAEncrypt(Key, SourceBytes);
// Исходные данные и ключ больше не нужны - затираем
SecureFree(Key);
SecureFree(SourceBytes);
finally
// На всякий случай (исключение) - затёрли и освободили всё
SecureFree(Source);
SecureFree(SourceBytes);
SecureFree(Key);
end;
// Далее у нас есть:
// Salt - соль для конвертации пароля в ключ
// EncryptedBytes - зашифрованные данные произвольного размера
// Именно эти данные нужно передать на сторону для расшифровки
// Шаг 2: расшифровка
try
// Получили ключ из пароля (Salt нам должны передать)
Key := TEADeriveKeyHMAC('супер секретный пароль', Salt);
// Здесь Key должен совпасть с ключом,
// который мы использовали для шифрования
// Расшифровали данные
SourceBytes := TEADecrypt(Key, EncryptedBytes);
// Удалили ключ
SecureFree(Key);
// Опционально
SecureFree(EncryptedBytes);
// Декодировали исходные данные
// В данном случае - из UTF-8
Source := UTF8ToString(SourceBytes);
// Исходные данные больше не нужны - затираем
SecureFree(SourceBytes);
// Как-то использовали исходные данные
ShowMessage(Source);
// Исходные данные больше не нужны - затираем
SecureFree(Source);
finally
// На всякий случай (исключение) - затёрли и освободили всё
SecureFree(Source);
SecureFree(SourceBytes);
SecureFree(EncryptedBytes);
SecureFree(Key);
SecureFree(Salt);
end;
end;
2. Обмен зашифрованными данными между Delphi и PHP:
uses
EEncrypt, // для всех Twofish функций и SecureFree
EEncoding, // для Base64EncodeToString
ECore; // для ShellExec
procedure TForm1.Button1Click(Sender: TObject);
const
// Секретный ключ, известный обоим сторонам
// Должен быть тем же самым, что и в PHP скрипте
// Это просто случайный набор байт, полученный вызовом TwofishInitSessionKeyRAW
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
SecretKey: TTwofishRAWKey =
(160, 22, 228, 9, 73, 192, 173, 149,
154, 19, 115, 215, 74, 36, 20, 202,
178, 26, 103 , 47, 51, 4, 144, 20,
73, 153, 49, 160, 192, 25, 20, 114);
var
Key: TTwofishKey; // Оптимизированный ключ
IV: TTwofishInitVector; // Вектор инициализации
Text: String; // Исходный текст
TextRAW: RawByteString; // Байтовое представление исходного текста
EncryptedText: RawByteString; // Двоичное представление зашифрованного текста
EncodedIV: String; // Кодированный в текст вектор инициализации
EncodedText: String; // Кодированный в текст зашифрованный текст
URL: String; // URL для вызова PHP
ReplyRAW: RawByteString; // Ответ от PHP (байты)
Reply: String; // Ответ от PHP (текст)
begin
// Готовим буферы
FillChar(Key, SizeOf(Key), 0);
FillChar(IV, SizeOf(IV), 0);
try
// Откуда-то получаем исходный текст
Text := 'Привет!';
// Конвертируем текст в байты. В данном случае - UTF-8
TextRAW := UTF8Encode(Text);
// Затрём исходный текст
SecureFree(Text);
// Оптимизируем ключ
Key := TwofishInitKey(SecretKey);
// Сгенерируем случайные байты для использования в качестве вектора инициализации
IV := TwofishInitIV;
// Шифруем открытый текст
// Поскольку используется вектор инициализации,
// то шифрование происходит в режиме CBC
EncryptedText := TwofishEncrypt(Key, TextRAW, @IV);
// Затрём исходный текст и ключ
SecureFree(TextRAW);
// Кодируем в текст двоичные данные
EncodedIV := Base64EncodeToString(@IV, SizeOf(IV));
EncodedText := Base64EncodeString(EncryptedText);
// Опционально
SecureFree(EncryptedText);
// Формируем URL для вызова PHP скрипта
// URLEncode нужно для экранирования символа '+'
// Если вместо Base64 вы будете кодировать в HEX, то URLEncode не нужно
URL := Format('http://localhost/test.php?iv=%s&text=%s', [URLEncode(EncodedIV), URLEncode(EncodedText)]);
// Опционально
SecureFree(EncodedIV);
SecureFree(EncodedText);
// Передаём в PHP
if not InitWebTools then
RaiseLastOSError;
try
ReplyRAW := InternetGet(URL, [], []);
finally
DoneWebTools;
end;
// PHP-скрипт ничего не вернул?
if ReplyRAW = '' then
Abort;
// Опционально
SecureFree(URL);
// Декодируем текст в байты
ReplyRAW := Base64DecodeString(Trim(String(ReplyRAW)));
// Расшифровываем ответ
TextRAW := TwofishDecrypt(Key, ReplyRAW, @IV);
// Удаляем ключ и вектор
SecureFree(Key);
SecureFree(IV);
// Опционально
SecureFree(ReplyRAW);
// Конвертируем двоичное представление в текст, в данном случае - UTF-8
Text := UTF8ToString(TextRAW);
// Зануляем ответ
SecureFree(TextRAW);
// Как-то используем ответ
ShowMessage(Text);
// Покажет:
// 'Hello from PHP: Привет'
// Зануляем ответ
SecureFree(Text);
finally
// На всякий случай (исключение) - затираем всё
SecureFree(Text);
SecureFree(TextRAW);
SecureFree(Key);
SecureFree(IV);
SecureFree(EncryptedText);
SecureFree(EncodedIV);
SecureFree(EncodedText);
SecureFree(URL);
SecureFree(ReplyRAW);
SecureFree(Reply);
end;
end;
<?php
// Эти функции необходимы, потому что MCrypt использует дополнение нулями вместо PKCS#5
// OpenSSL поддерживает PKCS#5, но не поддерживает Twofish
// Дополнение данных по PKCS#5
function pkcs5_pad($text, $blocksize = 16) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
// Обрезка PKCS#5
function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}
// Секретный ключ, известный обоим сторонам
// Должен быть тем же самым, что и в Delphi
// Это просто случайный набор байт, полученный вызовом random_bytes(32)
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
$Key = pack('C*',
160, 22, 228, 9, 73, 192, 173, 149,
154, 19, 115, 215, 74, 36, 20, 202,
178, 26, 103 , 47, 51, 4, 144, 20,
73, 153, 49, 160, 192, 25, 20, 114);
// Читаем переданные данные (вектор инициализации и зашифрованный текст)
$EncodedIV = $_GET['iv'];
$EncodedText = $_GET['text'];
// Декодируем текст в байты
$IV = base64_decode($EncodedIV);
$EncryptedText = base64_decode($EncodedText);
// Расшифровываем текст (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$Text = mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedText, MCRYPT_MODE_CBC, $IV);
// Обрезаем данные до их реального размера
$Text = pkcs5_unpad($Text);
// Что-то делаем с открытым текстом из Delphi
$Text = 'Hello from PHP: ' . $Text;
// Дополняем данные до размера, кратного блоку шифра
$Text = pkcs5_pad($Text);
// Зашифровываем ответ (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$EncryptedText = mcrypt_encrypt(MCRYPT_TWOFISH, $Key, $Text, MCRYPT_MODE_CBC, $IV);
// Кодируем зашифрованные байты в текст
$EncodedText = base64_encode($EncryptedText);
// Возвращаем ответ в Delphi
echo($EncodedText);
3. Хранение файла в зашифрованном виде:
uses
EEncrypt, // для всех Twofish функций и SecureFree
EEncoding, // для HexEncodeToString/HexDecodeString
EConfig, // для RegKeyWrite/RegKeyRead
ECompatibility; // для FileToString/StringToFile
procedure TForm1.Button1Click(Sender: TObject);
var
RAWKey: TTwofishRAWKey; // Ключ для шифрования и дешифрования (исходный)
Key: TTwofishKey; // Ключ для шифрования и дешифрования (оптимизированный)
IV: TTwofishInitVector; // Вектор инициализации
Content: RawByteString; // Исходные байты файла
EncryptedData: RawByteString; // Зашифрованный файл
DataClear: RawByteString; // Для шифрования ключа шифрования
DataEncrypted: RawByteString; // Для шифрования ключа шифрования
DataStr: String; // Строковое представление зашифрованного ключа шифрования
begin
// Обнуляем все буферы
FillChar(RAWKey, SizeOf(RAWKey), 0);
FillChar(Key, SizeOf(Key), 0);
FillChar(IV, SizeOf(IV), 0);
// Шаг 1: шифрование файла случайным ключём
try
// Создали случайный ключ
RAWKey := TwofishInitSessionKeyRAW;
// Оптимизировали ключ
Key := TwofishInitKey(RAWKey);
// Загружаем файл в двоичную строку
Content := FileToString(ParamStr(0));
// В Content будет 'MZP'#0#2#0#0#0...
// Создали случайный набор байтов для использования в качестве вектора
IV := TwofishInitIV;
// Шифруем данные (файл)
EncryptedData := TwofishEncrypt(Key, Content, @IV);
// Чистим и удаляем ненужные данные
SecureFree(Content);
SecureFree(Key);
finally
// На всякий случай (исключение) - чистим данные
SecureFree(Content);
SecureFree(Key);
end;
// Далее у нас есть:
// RAWKey - (секретный) ключ, которым мы шифровали данные
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные
// Шаг 2: защита ключа шифрования
try
// Конвертируем ключ шифрования в RawByteString для удобства
DataClear := RAWToString(@RAWKey, SizeOf(RAWKey));
// Чистим исходный ключ шифрования
SecureFree(RAWKey);
// Шифруем ключ шифрования
DataEncrypted := DataProtect(DataClear);
// Чистим исходный ключ шифрования
SecureFree(DataClear);
// Преобразовали зашифрованный ключ в текст
DataStr := HexEncodeToString(Pointer(DataEncrypted), Length(DataEncrypted));
// Опционально
SecureFree(DataEncrypted);
// Сохранили зашифрованный ключ в реестре
// Мы можем это сделать, поскольку ключ сможет расшифровать только этот же пользователь
RegKeyWrite(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'EncryptionKey', DataStr);
// Опционально
SecureFree(DataStr);
finally
// На всякий случай (исключение) - чистим данные
SecureFree(RAWKey);
SecureFree(DataClear);
SecureFree(DataEncrypted);
SecureFree(DataStr);
end;
// Далее у нас есть:
// Зашифрованный (секретный) ключ в реестре
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные
// Если нам нужно сохранить зашифрованные данные на диск, то мы сохраняем IV и EncryptedData
// Расшифровать данные сможет только текущий пользователь и никто иной
// Шаг 3: расшифровка ключа шифрования
try
// Прочитали зашифрованный ключ шифрования (в виде текста)
DataStr := RegKeyRead(HKEY_CURRENT_USER, 'SOFTWAREYourApp', 'EncryptionKey', '');
// Декодировали текст в двоичные данные
DataEncrypted := HexDecodeString(DataStr);
// Опционально
SecureFree(DataStr);
// Расшифровали зашифрованный ключ
DataClear := DataUnprotect(DataEncrypted);
// Опционально
SecureFree(DataEncrypted);
// Подготовили расшифрованный ключ
Assert(Length(DataClear) = SizeOf(RAWKey));
RAWFromString(DataClear, @RAWKey);
// Удалили копию расшифрованного ключа
SecureFree(DataClear);
finally
// На всякий случай (исключение) - чистим данные
SecureFree(DataStr);
SecureFree(DataEncrypted);
SecureFree(DataClear);
end;
// Далее у нас есть:
// RAWKey - (секретный) ключ, которым мы шифровали данные
// IV - (открытый) вектор инициализации
// EncryptedData - зашифрованные данные
// Шаг 4: расшифровываем файл
try
// Оптимизировали ключ
Key := TwofishInitKey(RAWKey);
// Чистим ненужный ключ
SecureFree(RAWKey);
// Расшифровываем данные (файл)
// Здесь Key и IV должны быть теми же самыми, что использовались для шифрования
Content := TwofishDecrypt(Key, EncryptedData, @IV);
// Чистим ненужный ключ
SecureFree(Key);
// Опционально
SecureFree(EncryptedData);
// Сохранили расшифрованные данные в файл
StringToFile(ParamStr(0) + '.copy', Content);
// Здесь файл Project1.exe.copy получится в точности равным Project1.exe
// Затрём исходные данные
SecureFree(Content);
finally
// На всякий случай (исключение) - чистим данные
SecureFree(RAWKey);
SecureFree(Key);
SecureFree(EncryptedData);
SecureFree(Content);
end;
end;
Асимметричная криптография
Для асимметричного шифрования EurekaLog поддерживает RSA. Алгоритм не патентован и может быть свободно использован в любой программе. Асимметричность алгоритма означает, что для шифрования и расшифровки используются два разных ключа. Один из ключей держат в секрете, он называется закрытым или приватным, а другой можно публиковать — он называется открытым. Зашифрованное открытым ключом можно расшифровать только закрытым ключом и наоборот.
Управление ключами
В EurekaLog ключи RSA хранятся в записи TRSAKey
, которая имеет два поля: PublicKey
для хранения открытого ключа и PrivateKey
для хранения закрытого (секретного) ключа. Новую пару ключей можно сгенерировать функцией RSAGenKey
(выполняется очень долго; читай: 5-15 секунд). Как правило, ключи не генерируются в программах, а загружаются уже готовые (заранее сгенерированные). Для сохранения загрузки ключей в EurekaLog есть функции RSALoad/SavePublic/PrivateKey
, например: RSALoadPrivateKey
. Для импорта/экспорта EurekaLog поддерживает несколько форматов, описываемых TRSAExport/TRSAImport
:
rsBLOB
— это двоичное представление ключа с заголовком. В качестве заголовка используются структурыPUBLICKEYBLOB/PRIVATEKEYBLOB
Microsoft. Little-Endian.rsDER
— это двоичное представление ключа, кодированное в контейнер ASN.1 без опционального заголовка (формат PKCS#1), Big-Endian. При загрузке опциональный заголовок ASN.1 от PKCS#8 (например, после экспорта из PHP/OpenSSL) будет пропущен. В CryptoAPI соответствуетRSA_CSP_PUBLICKEYBLOB/PKCS_RSA_PRIVATE_KEY
. Как правило, такой формат сохраняется в файлы с расширением.der
.rsPEM
— это текстовое представление ключа. По сути, это тот жеrsDER
, но кодированный в Base64. Как правило, такой формат сохраняется в файлы с расширением.pem
,.key
,.cert
,.cer
или.crt
. Файлы в формате PKCS#1 используют заголовки-----BEGIN/END RSA PRIVATE KEY-----
,-----BEGIN/END RSA PUBLIC KEY-----
, а файлы в формате PKCS#8 используют заголовки-----BEGIN/END PRIVATE KEY-----
,-----BEGIN/END PUBLIC KEY-----
.
Зашифрованные ключи PKCS#1/PKCS#8 в EurekaLog не поддерживаются. Форматы контейнеров PKCS#7 и PKCS#12 в EurekaLog не поддерживаются. PKCS#8 поддерживается EurekaLog только на импорт (см. ниже).
Например, один и тот же публичный ключ экспортируется следующим образом:
rsBLOB
(заголовокPUBLICKEYBLOB/PRIVATEKEYBLOB
+ little-endian, ключ $25 $17 $B4 $A0 … $96 $B9 $9C $E7 начинается с $15/21-го байта и до конца):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: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; - На 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:Documentspublic.pem', rsPEM);
RSASavePrivateKey(Key, 'C:Documentsprivate.pem', rsPEM);
finally
// Удалили ключи
SecureFree(Key);
end;
end; - Импортировать ключи в 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); - Импортировать ключи в 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:Documentsprivate.pem', rsPEM);var
RSAKey: TRSAKey;
begin
// Импорт PKCS#8 PEM
RSAKey := RSALoadPublicKey('C:Documentspublic.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:Documentspublic.pem', rsPEM);
try
// Шифруем симметричный ключ SessionKeyRAW в буфер EncryptedData
EncryptedData.cbData := SizeOf(SessionKeyRAW);
RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData);
finally
// Как только мы его зашифровали - нам больше не нужен асимметричный ключ
SecureFree(RSAKey);
// Также не нужна и исходная форма ключа
SecureFree(SessionKeyRAW);
end;
try
// Здесь: EncryptedData хранит зашифрованный симметричный ключ
// Сохраним зашифрованный ключ шифрования в сам выходной файл
FS := TFileStream.Create('C:DocumentsEncryptedData.bin', fmCreate or fmShareExclusive);
FS.WriteBuffer(EncryptedData.cbData, SizeOf(EncryptedData.cbData));
FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData);
// Не закрываем FS, мы ещё не записали туда сами данные...
finally
// Удаляем зашифрованный ключ
SecureFree(EncryptedData);
end;
// Далее, у нас есть симметричный ключ шифрования SessionKey и открытый выходной файл FS
// Шаг 2: шифруем данные
Data := TMemoryStream.Create;
try
// Загрузим открытый текст для шифрования
Data.LoadFromFile('C:DocumentsText.txt');
// Шифруем файл
// Поскольку ключ шифрования - случайный, то вектор инициализации не используется
// Поэтому шифрование происходит в режиме ECB
EncryptedData.cbData := Cardinal(Data.Size);
TwofishEncrypt(SessionKey, Data.Memory, Pointer(EncryptedData.pbData), EncryptedData.cbData);
// Ключ больше не нужен
SecureFree(SessionKey);
// Сохраняем зашифрованные данные в файл
FS.WriteBuffer(EncryptedData.pbData^, EncryptedData.cbData);
finally
// Удаляем данные
SecureFree(Data);
SecureFree(EncryptedData);
end;
finally
// Закрываем файл
FreeAndNil(FS);
end;
// Теперь файл C:DocumentsEncryptedData.bin полностью готов
// В нём лежит зашифрованный (случайный) симметричный ключ, а также зашифрованный файл C:DocumentsText.txt
// Его сможет расшифровать только тот, у кого есть закрытый асимметричный ключ, соответствующий использованному открытому асимметричному ключу
// Т.е. файл можно передать получателю любым способом, в том числе - по открытым каналам связи
finally
// Чистим всё
SecureFree(SessionKey);
end;
end;
procedure TForm1.Button2Click(Sender: TObject);
var
// Случайный симметричный ключ для шифрования данных
SessionKey: TTwofishKey;
// Асимметричный ключ для шифрования симметричного ключа
RSAKey: TRSAKey;
// Открытые данные для шифрования
Data: TMemoryStream;
// Расшифрованные данные
DecryptedData: TEncryptBuffer;
// Поток для сохранения данных
FS: TFileStream;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(DecryptedData, SizeOf(DecryptedData), 0);
// Открываем зашифрованный файл
FS := TFileStream.Create('C:DocumentsEncryptedData.bin', fmOpenRead or fmShareDenyWrite);
try
try
// Шаг 1: расшифровываем сессионный ключ
// Сначала читаем из файла зашифрованный (случайный) симметричный ключ
FS.ReadBuffer(DecryptedData.cbData, SizeOf(DecryptedData.cbData));
DecryptedData.pbData := AllocMem(DecryptedData.cbData);
try
FS.ReadBuffer(DecryptedData.pbData^, DecryptedData.cbData);
// Расшифровываем симметричный ключ с помощью закрытого асимметричного ключа
RSAKey := RSALoadPrivateKey('C:Documentsprivate.pem', rsPEM);
try
EEncrypt.RSADecrypt(RSAKey, DecryptedData);
finally
SecureFree(RSAKey);
end;
// Инициализируем симметричный ключ
Assert(DecryptedData.cbData = SizeOf(TTwofishRAWKey));
SessionKey := TwofishInitKey(TTwofishRAWKey(Pointer(DecryptedData.pbData)^));
finally
// Удаляем ненужные данные
SecureFree(DecryptedData);
end;
// Теперь у нас есть симметричный ключ SessionKey, которым мы можем расшифровать файл
// Шаг 2: расшифровываем сами данные
Data := TMemoryStream.Create;
try
// Остаток в файле - это сам зашифрованный файл
Data.CopyFrom(FS, FS.Size - FS.Position);
// Расшифровываем файл
DecryptedData.cbData := Cardinal(Data.Size);
TwofishDecrypt(SessionKey, Data.Memory, Pointer(DecryptedData.pbData), DecryptedData.cbData);
// Ключ больше не нужен
SecureFree(SessionKey);
// Сохраняем расшифрованный результат в файл
FreeAndNil(FS);
FS := TFileStream.Create('C:DocumentsText2.txt', fmCreate or fmShareExclusive);
try
FS.WriteBuffer(DecryptedData.pbData^, DecryptedData.cbData);
finally
FreeAndNil(FS);
end;
// Данные больше не нужны
SecureFree(DecryptedData);
// Здесь: Text2.txt должен стать точной копией Text.txt
finally
SecureFree(Data);
end;
finally
// На всякий случай (выход по исключению) - затираем всё
SecureFree(SessionKey);
SecureFree(DecryptedData);
end;
finally
FreeAndNil(FS);
end;
end;
Цифровая подпись
Когда для шифрования используется закрытый ключ, а для расшифровки используется открытый — это называется «цифровая подпись». Поскольку расшифровать данные может любой (т.к. открытый ключ есть у всех), то секретность данных таким способом не обеспечивается. Но зато, если данные удаётся расшифровать чьим-то открытым ключом — мы можем быть уверены, что данные были зашифрованны именно им (ибо секретный закрытый ключ есть только у этого лица). Таким образом, мы можем проверить аутентичность данных.
При этом как правило сами данные не шифруются. Вместо этого от данных вычисляют хэш и его шифруют. Для подписи произвольных данных и её проверки в EurekaLog есть функции RSASign
(использует закрытый ключ) и RSAVerify
(использует открытый ключ). Функции цифровой подписи EurekaLog используют SHA1 со схемой EMSA-PKCS1 для дополнения данных.
Результирующая цифровая подпись представляет собой непрозрачный набор байт произвольной длины. Если вы захотите обмениваться цифровой подписью с другими окружениями — нужно помнить, что Windows/Delphi используют little endian порядок байт, в то время как некоторые другие окружения (например, .NET или PHP) используют big endian. Поэтому в некоторых случаях порядок байт цифровой подписи нужно обратить.
Например:
Запрос лицензии от PHP-скрипта:
uses
EEncrypt, // для всех RSA функций и SecureFree
EEncoding, // для Base64
EJSON, // для JSON функций
EWebTools; // для интернет-функций
procedure TForm1.Button1Click(Sender: TObject);
var
// Данные для отправки PHP-скрипту
JSON, JSONRequest, JSONUser: IJSONValues;
JSONText: String;
JSONRAW: RawByteString;
// Случайный симметричный ключ для шифрования данных (сессионный ключ)
SessionKey: TTwofishKey;
SessionKeyRAW: TTwofishRAWKey;
// Асимметричный ключ для шифрования сессионного ключа
RSAKey: TRSAKey;
// Зашифрованные данные
EncryptedData: TEncryptBuffer;
// Текстовое представление зашифрованных данных
EncodedKey, EncodedData: String;
// URL для вызова PHP
URL: String;
// Ответ от PHP (байты)
ReplyRAW: RawByteString;
// Текстовое представление лицензии
EncodedLicense: String;
// Текстовое представление цифровой подписи
EncodedSignature: String;
// Лицензия (байты)
License: RawByteString;
// Цифровая подпись (байты)
Signature: RawByteString;
begin
// Обнулим все буферы
FillChar(SessionKey, SizeOf(SessionKey), 0);
FillChar(SessionKeyRAW, SizeOf(SessionKeyRAW), 0);
FillChar(RSAKey, SizeOf(RSAKey), 0);
FillChar(EncryptedData, SizeOf(EncryptedData), 0);
try
// Шаг 1: готовим данные для отправки PHP-скрипту
JSON := JSONCreate;
JSONRequest := JSONCreate;
JSONUser := JSONCreate;
JSONRequest['version'] := 1;
JSONRequest['type'] := 'license';
JSONRequest['scope'] := 'installer';
JSONUser['login'] := 'input-from-edit1';
JSONUser['password'] := 'input-from-edit2';
JSON['app'] := 'MyApp';
JSON['version'] := GetModuleVersion(GetModuleName(HInstance));
JSON['date'] := Now;
JSON['request'] := JSONRequest;
JSON['user'] := JSONUser;
JSONText := JSON.ToString;
Finalize(JSONUser); // не обязательно
Finalize(JSONRequest); // не обязательно
Finalize(JSON); // не обязательно
(*
Здесь JSONText содержит:
{
"app": "MyApp",
"version": "1.0.0.0",
"date": "2021.06.25 14:04:21",
"request": {
"version": 1,
"type": "license",
"scope": "installer"
}
"user": {
"login": "input-from-edit1",
"password": "input-from-edit2"
}
}
*)
JSONRAW := UTF8Encode(JSONText);
SecureFree(JSONText);
// Здесь JSONRAW содержит байты для отправки PHP-скрипту
// Шаг 2: шифруем данные запроса и отправляем PHP-скрипту
// Создадим случайный симметричный ключ
SessionKeyRAW := TwofishInitSessionKeyRAW;
SessionKey := TwofishInitKey(SessionKeyRAW);
// Шаг 2а: шифруем ключ шифрования
// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try
// Шифруем симметричный ключ SessionKeyRAW в буфер EncryptedData
EncryptedData.cbData := SizeOf(SessionKeyRAW);
RSAEncrypt(RSAKey, @SessionKeyRAW, Pointer(EncryptedData.pbData), EncryptedData.cbData);
try
// Как только мы его зашифровали - нам больше не нужен асимметричный ключ
SecureFree(RSAKey);
// Также не нужна и исходная форма ключа
SecureFree(SessionKeyRAW);
// Кодируем зашифрованные данные в текст
EncodedKey := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData);
// Удаляем ненужный данные
SecureFree(EncryptedData);
finally
// На всякий случай (исключение) - чистим данные
SecureFree(EncryptedData);
end;
finally
// На всякий случай (исключение) - чистим данные
SecureFree(RSAKey);
SecureFree(SessionKeyRAW);
end;
// Здесь: EncodedKey хранит зашифрованный симметричный ключ (в текстовом виде)
// Далее, у нас есть симметричный ключ шифрования SessionKey и данные для отправки JSONRAW
// Шаг 2б: шифруем данные запроса
// Поскольку ключ шифрования - случайный, то вектор инициализации не используется
// Поэтому шифрование происходит в режиме ECB
EncryptedData.cbData := Length(JSONRAW);
TwofishEncrypt(SessionKey, Pointer(JSONRAW), Pointer(EncryptedData.pbData), EncryptedData.cbData);
try
// Ключ больше не нужен
SecureFree(SessionKey);
// Кодируем зашифрованные данные в текст
EncodedData := Base64EncodeToString(EncryptedData.pbData, EncryptedData.cbData);
// Удаляем ненужные данные
SecureFree(EncryptedData);
// Отправляем зашифрованные данные и ключ скрипту
// Здесь URLEncode нужно для экранирования '+'
URL := 'http://localhost/test.php?key=' + URLEncode(EncodedKey) + '&data=' + URLEncode(EncodedData);
if not InitWebTools then
RaiseLastOSError;
try
ReplyRAW := InternetGet(URL, [], []);
finally
DoneWebTools;
end;
// PHP-скрипт ничего не вернул?
if ReplyRAW = '' then
Abort;
// Опционально
SecureFree(URL);
// Удаляем ненужные данные
SecureFree(EncodedKey);
SecureFree(EncodedData);
finally
// На всякий случай (исключение) - чистим данные
SecureFree(SessionKey);
SecureFree(EncryptedData);
SecureFree(EncodedKey);
SecureFree(EncodedData);
end;
finally
// На всякий случай (исключение) - чистим данные
SecureFree(SessionKey);
SecureFree(SessionKeyRAW);
SecureFree(EncodedData);
SecureFree(EncodedKey);
SecureFree(JSONText);
SecureFree(JSONRAW);
end;
// Здесь у нас есть ReplyRAW - ответ от PHP-скрипта (байты)
try
// Конвертируем байты в текст
JSONText := UTF8ToString(ReplyRAW);
// Опционально
Finalize(ReplyRAW);
// Конвертируем текст JSON в объект JSON
JSON := JSONCreate(JSONText);
// Опционально
Finalize(JSONText);
// PHP-скрипт вернул ошибку?
if JSON.IndexOf('error') >= 0 then
raise Exception.Create(JSON['error']);
// Читаем лицензию и подпись
EncodedLicense := JSON['license'];
EncodedSignature := JSON['signature'];
// Текст в байты
License := Base64DecodeString(EncodedLicense);
Signature := Base64DecodeString(EncodedSignature);
// Загружаем открытый ключ из файла
RSAKey := RSALoadPublicKey('C:Documentspublic.pem', rsPEM);
try
// Проверяем цифровую подпись
if RSAVerify(RSAKey, Pointer(License), Length(License), Pointer(Signature), Length(Signature)) then
begin
// Цифровая подпись не нарушена
// Мы можем быть уверены, что License действительно пришла с нашего сервера
// Опционально
SecureFree(RSAKey);
// Просто для примера
// В реальной программе License была бы зашифрована
ShowMessage(UTF8ToString(License));
// Покажет 'This is just an example license'
end
else
ShowMessage('Not signed');
finally
// На всякий случай (исключение) - чистим данные
SecureFree(RSAKey);
end;
finally
// На всякий случай (исключение) - чистим данные
SecureFree(JSONText);
SecureFree(ReplyRAW);
SecureFree(EncodedSignature);
SecureFree(EncodedLicense);
SecureFree(Signature);
SecureFree(License);
end;
end;
<?php
// Эти функции необходимы, потому что MCrypt использует дополнение нулями вместо PKCS#5
// OpenSSL поддерживает PKCS#5, но не поддерживает Twofish
// Дополнение данных по PKCS#5
function pkcs5_pad($text, $blocksize = 16) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
// Обрезка PKCS#5
function pkcs5_unpad($text) {
$pad = ord($text{strlen($text)-1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}
// Секретный ключ
// Должен соответствовать открытому ключу, используемому в Delphi
// Если вы будете использовать этот код, то, конечно же, должны заменить этот ключ на свой
// Это только для примера. В реальной программе ключ может где-то храниться
$PrivateKey = <<<EOD
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDkIEGZvxCUNW3u
...
X+VZcAA+CUNp1xYUuep0UoeqYtfzTuKvinNo3PWAItyqtCJNcdNWDzbWRizMEPJK
N9AYlDckmF/X/1cZxWNyiltZZ542kezn
-----END PRIVATE KEY-----
EOD;
// Читаем переданные данные (зашифрованный сессионный ключ и зашифрованные данные)
$EncodedKey = $_GET['key'];
$EncodedData = $_GET['data'];
// Декодируем текст в байты
$EncryptedKey = base64_decode($EncodedKey);
$EncryptedData = base64_decode($EncodedData);
// Переводим little-endian (Windows/EurekaLog/WinCrypt) в big-endian (PHP/OpenSSL)
$EncryptedKey = strrev($EncryptedKey);
// Расшифровываем сессионный ключ
if (!openssl_private_decrypt($EncryptedKey, $Key, $PrivateKey)) {
echo('{ "error": ' . json_encode('openssl_private_decrypt: ' . openssl_error_string()) . ' }');
die();
}
// Расшифровываем текст (в некоторых версиях PHP: MCRYPT_TWOFISH256 вместо MCRYPT_TWOFISH)
$Data = pkcs5_unpad(mcrypt_decrypt(MCRYPT_TWOFISH, $Key, $EncryptedData, MCRYPT_MODE_ECB));
// Преобразуем строку JSON в объект JSON
$Data = json_decode($Data, true);
// Проверяем тип запроса
$Request = $Data['request'];
if (($Request['version'] < 1) || ($Request['version'] > 1)) {
echo('{ "error": "Unsupported request" }');
die();
}
// Запрашивают лицензию?
if ($Request['type'] == 'license') {
// Проверяем пользователя
$User = $Data['user'];
// Только для примера
$OK = (($User['login'] == 'input-from-edit1') && ($User['password'] == 'input-from-edit2'));
if ($OK) {
// Каким-то образом получаем лицензию для указанного пользователя
// В реальной программе это будут, вероятно, зашифрованные данные
$License = 'This is just an example license';
// Подписываем лицензию
openssl_sign($License, $Signature, $PrivateKey, OPENSSL_ALGO_SHA1);
// Переводим big-endian (PHP/OpenSSL) в little-endian (Windows/EurekaLog/WinCrypt)
$Signature = strrev($Signature);
// Кодируем байты в текст
$EncodedLicense = base64_encode($License);
$EncodedSignature = base64_encode($Signature);
// Возвращаем вызывающему лицензию с подписью
echo('{ "license": ' . json_encode($EncodedLicense) . ', "signature": ' . json_encode($EncodedSignature) . ' }');
} else {
echo('{ "error": "Access Denied" }');
die();
}
}
echo('{ "error": "Unsupported request" }');
Работа с криптографией в EurekaLog
EurekaLog — это трейсер исключений, т.е. инструмент, который устанавливает хуки и перехватывает возбуждение исключений, позволяя создать отчёт по необработанным исключениям. Тем не менее, в ней есть различного рода дополнительная функциональность, которую вы можете использовать в своих программах. И одной из таких возможностей являются функции криптографии.
EurekaLog предлагает три модуля:
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 — это неплохая хэш-функция общего назначения с равномерным распределением, удобна для использования в качестве ключа/индекса в БД
Все функции хэширования имеют имя ИмяХэша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-го байта и до конца):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: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; - На 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:Documentspublic.pem', rsPEM);
RSASavePrivateKey(Key, 'C:Documentsprivate.pem', rsPEM);
finally
// Удалили ключи
SecureFree(Key);
end;
end; - Импортировать ключи в 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); - Импортировать ключи в 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:Documentsprivate.pem', rsPEM);var
RSAKey: TRSAKey;
begin
// Импорт PKCS#8 PEM
RSAKey := RSALoadPublicKey('C:Documentspublic.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: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" }');
Получение SHA1-хеша цифрового сертификата
Одним из вариантов сертификата при программном добавлении цифровой подписи в PDF документ может быть идентифицированный по SHA1-хешу сертификат из хранилища сертификатов Windows. Получение без использования
[Сервисы #1]: Мастер создания сервисов
Местонахождение: File -> New -> Other -> Delphi -> Android ServiceОписание: Создаёт основу для разработки Android сервиса Опции: Local Service – наиболее распространённый вид сервиса (службы),