Как AES-128 зашифровать строку с помощью пароля в Delphi и расшифровать в C#?
Я бы хотел, чтобы AES-128 зашифровал строку в Delphi с помощью пароля. Я хотел бы загрузить это на свой сервер и иметь возможность расшифровывать, используя тот же пароль в C#.
В Delphi я использую TurboPower LockBox 3:
function EncryptText_AES_128(input: string; password: string): string;
var
Codec: TCodec;
CipherText: AnsiString;
begin
Codec := TCodec.Create(nil);
try
Codec.CryptoLibrary := TCryptographicLibrary.Create(Codec);
//
Codec.StreamCipherId := BlockCipher_ProgID;
Codec.BlockCipherId := Format(AES_ProgId, [128]);
Codec.ChainModeId := CBC_ProgId;
//
Codec.Password := Password;
Codec.EncryptString(input, CipherText);
//
Result := string(CipherText);
finally
Codec.Free;
end;
end;
Как я могу расшифровать полученную строку в C#? Я могу изменить код Delphi. Ничего еще не в производстве. Я даже не застрял на использовании LockBox. Но я бы не хотел помещать это в DLL для P / Invoke.
(Мой пример показывает, что моя зашифрованная последовательность сама по себе является строкой. Это не является обязательным требованием для меня. Поток байтов в порядке.)
5 ответов
Наконец-то я нашел совместимое решение между Delphi и C# для AES-128. Это также работает на Wine. Вот мой код Delphi:
unit TntLXCryptoUtils;
interface
function AES128_Encrypt(Value, Password: string): string;
function AES128_Decrypt(Value, Password: string): string;
implementation
uses
SysUtils, Windows, IdCoderMIME, TntLXUtils;
//-------------------------------------------------------------------------------------------------------------------------
// Base64 Encode/Decode
//-------------------------------------------------------------------------------------------------------------------------
function Base64_Encode(Value: TBytes): string;
var
Encoder: TIdEncoderMIME;
begin
Encoder := TIdEncoderMIME.Create(nil);
try
Result := Encoder.EncodeBytes(Value);
finally
Encoder.Free;
end;
end;
function Base64_Decode(Value: string): TBytes;
var
Encoder: TIdDecoderMIME;
begin
Encoder := TIdDecoderMIME.Create(nil);
try
Result := Encoder.DecodeBytes(Value);
finally
Encoder.Free;
end;
end;
//-------------------------------------------------------------------------------------------------------------------------
// WinCrypt.h
//-------------------------------------------------------------------------------------------------------------------------
type
HCRYPTPROV = Cardinal;
HCRYPTKEY = Cardinal;
ALG_ID = Cardinal;
HCRYPTHASH = Cardinal;
const
_lib_ADVAPI32 = 'ADVAPI32.dll';
CALG_SHA_256 = 32780;
CALG_AES_128 = 26126;
CRYPT_NEWKEYSET = $00000008;
PROV_RSA_AES = 24;
KP_MODE = 4;
CRYPT_MODE_CBC = 1;
function CryptAcquireContext(var Prov: HCRYPTPROV; Container: PChar; Provider: PChar; ProvType: LongWord; Flags: LongWord): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptAcquireContextW';
function CryptDeriveKey(Prov: HCRYPTPROV; Algid: ALG_ID; BaseData: HCRYPTHASH; Flags: LongWord; var Key: HCRYPTKEY): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDeriveKey';
function CryptSetKeyParam(hKey: HCRYPTKEY; dwParam: LongInt; pbData: PBYTE; dwFlags: LongInt): LongBool stdcall; stdcall; external _lib_ADVAPI32 name 'CryptSetKeyParam';
function CryptEncrypt(Key: HCRYPTKEY; Hash: HCRYPTHASH; Final: LongBool; Flags: LongWord; pbData: PBYTE; var Len: LongInt; BufLen: LongInt): LongBool;stdcall;external _lib_ADVAPI32 name 'CryptEncrypt';
function CryptDecrypt(Key: HCRYPTKEY; Hash: HCRYPTHASH; Final: LongBool; Flags: LongWord; pbData: PBYTE; var Len: LongInt): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDecrypt';
function CryptCreateHash(Prov: HCRYPTPROV; Algid: ALG_ID; Key: HCRYPTKEY; Flags: LongWord; var Hash: HCRYPTHASH): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptCreateHash';
function CryptHashData(Hash: HCRYPTHASH; Data: PChar; DataLen: LongWord; Flags: LongWord): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptHashData';
function CryptReleaseContext(hProv: HCRYPTPROV; dwFlags: LongWord): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptReleaseContext';
function CryptDestroyHash(hHash: HCRYPTHASH): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDestroyHash';
function CryptDestroyKey(hKey: HCRYPTKEY): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDestroyKey';
//-------------------------------------------------------------------------------------------------------------------------
{$WARN SYMBOL_PLATFORM OFF}
function __CryptAcquireContext(ProviderType: Integer): HCRYPTPROV;
begin
if (not CryptAcquireContext(Result, nil, nil, ProviderType, 0)) then
begin
if HRESULT(GetLastError) = NTE_BAD_KEYSET then
Win32Check(CryptAcquireContext(Result, nil, nil, ProviderType, CRYPT_NEWKEYSET))
else
RaiseLastOSError;
end;
end;
function __AES128_DeriveKeyFromPassword(m_hProv: HCRYPTPROV; Password: string): HCRYPTKEY;
var
hHash: HCRYPTHASH;
Mode: DWORD;
begin
Win32Check(CryptCreateHash(m_hProv, CALG_SHA_256, 0, 0, hHash));
try
Win32Check(CryptHashData(hHash, PChar(Password), Length(Password) * SizeOf(Char), 0));
Win32Check(CryptDeriveKey(m_hProv, CALG_AES_128, hHash, 0, Result));
// Wine uses a different default mode of CRYPT_MODE_EBC
Mode := CRYPT_MODE_CBC;
Win32Check(CryptSetKeyParam(Result, KP_MODE, Pointer(@Mode), 0));
finally
CryptDestroyHash(hHash);
end;
end;
function AES128_Encrypt(Value, Password: string): string;
var
hCProv: HCRYPTPROV;
hKey: HCRYPTKEY;
lul_datalen: Integer;
lul_buflen: Integer;
Buffer: TBytes;
begin
Assert(Password <> '');
if (Value = '') then
Result := ''
else begin
hCProv := __CryptAcquireContext(PROV_RSA_AES);
try
hKey := __AES128_DeriveKeyFromPassword(hCProv, Password);
try
// allocate buffer space
lul_datalen := Length(Value) * SizeOf(Char);
Buffer := TEncoding.Unicode.GetBytes(Value + ' ');
lul_buflen := Length(Buffer);
// encrypt to buffer
Win32Check(CryptEncrypt(hKey, 0, True, 0, @Buffer[0], lul_datalen, lul_buflen));
SetLength(Buffer, lul_datalen);
// base 64 result
Result := Base64_Encode(Buffer);
finally
CryptDestroyKey(hKey);
end;
finally
CryptReleaseContext(hCProv, 0);
end;
end;
end;
function AES128_Decrypt(Value, Password: string): string;
var
hCProv: HCRYPTPROV;
hKey: HCRYPTKEY;
lul_datalen: Integer;
Buffer: TBytes;
begin
Assert(Password <> '');
if Value = '' then
Result := ''
else begin
hCProv := __CryptAcquireContext(PROV_RSA_AES);
try
hKey := __AES128_DeriveKeyFromPassword(hCProv, Password);
try
// decode base64
Buffer := Base64_Decode(Value);
// allocate buffer space
lul_datalen := Length(Buffer);
// decrypt buffer to to string
Win32Check(CryptDecrypt(hKey, 0, True, 0, @Buffer[0], lul_datalen));
Result := TEncoding.Unicode.GetString(Buffer, 0, lul_datalen);
finally
CryptDestroyKey(hKey);
end;
finally
CryptReleaseContext(hCProv, 0);
end;
end;
end;
end.
А вот мой код C#:
public class TntCryptoUtils
{
private static ICryptoTransform __Get_AES128_Transform(string password, bool AsDecryptor)
{
const int KEY_SIZE = 16;
var sha256CryptoServiceProvider = new SHA256CryptoServiceProvider();
var hash = sha256CryptoServiceProvider.ComputeHash(Encoding.Unicode.GetBytes(password));
var key = new byte[KEY_SIZE];
var iv = new byte[KEY_SIZE];
Buffer.BlockCopy(hash, 0, key, 0, KEY_SIZE);
//Buffer.BlockCopy(hash, KEY_SIZE, iv, 0, KEY_SIZE); // On the Windows side, the IV is always 0 (zero)
//
if (AsDecryptor)
return new AesCryptoServiceProvider().CreateDecryptor(key, iv);
else
return new AesCryptoServiceProvider().CreateEncryptor(key, iv);
}
public static string AES128_Encrypt(string Value, string Password)
{
byte[] Buffer = Encoding.Unicode.GetBytes(Value);
//
using (ICryptoTransform transform = __Get_AES128_Transform(Password, false))
{
byte[] encyptedBlob = transform.TransformFinalBlock(Buffer, 0, Buffer.Length);
return Convert.ToBase64String(encyptedBlob);
}
}
public static string AES128_Decrypt(string Value, string Password)
{
byte[] Buffer = Convert.FromBase64String(Value);
//
using (ICryptoTransform transform = __Get_AES128_Transform(Password, true))
{
byte[] decyptedBlob = transform.TransformFinalBlock(Buffer, 0, Buffer.Length);
return Encoding.Unicode.GetString(decyptedBlob);
}
}
}
В отличие от любой пламенной приманки троллей, LockBox 3 на самом деле является криптографической библиотекой хорошего качества. Соответствие стандартам LB3 безупречно. Если у вас могут возникнуть проблемы с совместимостью с другими языками и библиотеками, это связано с параметрами, выходящими за рамки стандарта. Если вы используете Lockbox на стороне Delphi, то вам просто нужно убедиться, что эти опции обрабатываются так же, как на стороне другого языка. Если это невозможно, вам следует выбрать другую библиотеку. Я буду иметь дело с каждым из этих вариантов ниже.
В альтернативных решениях нет ничего плохого (OpenSSL, CryptoAPI и Eldos). Некоторые из них могут быть черным ящиком. Это может быть проблемой для некоторых людей.
Преобразование пароля в ключ. AES-128 использует 16-байтовый ключ. Также стандартный механизм генерации ключа из "данных ключа" или "данных пароля" изначально основан на 16-байтовом начальном семени ввода. Для обеспечения совместимости безопаснее генерировать двоичный ключ из строкового пароля на стороне Delphi и просто переносить двоичный ключ на другую сторону, а не переносить строковый пароль. Это связано с тем, что алгоритм преобразования строкового пароля в двоичный 16-байтовый ключ находится за пределами стандарта AES. Тем не менее, вы можете сделать это в любом случае. Когда lockbox получает строковый пароль для инициализации кодека AES-128, он рассматривает полезную нагрузку строки как массив байтов. Если полезная нагрузка составляет ровно 16 байтов, то она велика, и ее можно передать непосредственно алгоритму генерации ключа AES, который указан в стандарте. Если полезная нагрузка строки не является точно 16 байтами, то полезная нагрузка будет хешироваться с помощью SHA-1 для получения 20-байтового хеш-вывода. Младшие 16 байтов этого хэша затем передаются в стандартную функцию генерации ключа AES. Итак, ваши варианты обеспечения взаимодействия в отношении инициализации ключа:
1.1. Транспортируйте двоичные ключи вместо строковых паролей.
1.2. Если вариант 1.2 слишком неудобен, перенесите пароль, но имитируйте тот же алгоритм "пароль-ключ" с другой стороны.
1.3. Если 1 и 2 по какой-то причине не работают, попробуйте ограничить пароли ровно 16 байтами (8 символов UTF-8 или 16 кодовых точек UTF-16). Это должно быть довольно безопасно, если реализация на другом языке наполовину приличная.
UTF-16 против паролей ansi-string/UTF-8 Это не столько вариант, сколько ловушка для молодых игроков. Мы, программисты, склонны думать о "строках" как о "строках". Но это не так. В Delphi 2010 полезная нагрузка строк хранится в кодировке UTF-16LE с размером кодовой единицы 2 байта. Но в других языках, таких как PHP и python, в режиме по умолчанию строки представляют собой однобайтовые кодовые единицы кодирования, либо UTF-8, либо что-то на основе базы кодовых страниц MS Windows (которую MS называет "ansistring"). Стоит помнить, что кодировка mypassword UTF-16 отличается от кодировки mypassword UTF-8.
IV установка. Стандарт AES не касается вопроса о том, как настроить вектор инициализации кодека (IV). Размер IV такой же, как размер основного блока. Для AES это 128 бит или 16 байтов. При шифровании lockbox создает 16-байтовый одноразовый номер. Этот одноразовый номер становится значением IV, и он отправляется открытым текстом в заголовке сообщения зашифрованного текста. Прочитайте документацию по методу / политике другой стороны для инициализации IV. Ваши варианты:
3.1 Если другая сторона добавляет IV к зашифрованному тексту, то вы сладки.
3.2 В противном случае, с другой стороны, при расшифровке, прочитайте первые 16 байтов зашифрованного текста самостоятельно и передайте оставшуюся часть в чужой кодек. Перед расшифровкой расскажите иностранному кодеку, что такое IV (при условии, что его API способен на это).
Квантование блока Размер блока AES составляет 16 байтов. Когда текстовое сообщение не является целым кратным 16 байтами, необходимо что-то сделать, чтобы сделать его целым кратным. Эта процедура называется блочным квантованием и не рассматривается в стандарте, а оставлена на усмотрение. Многие реализации будут использовать заполнение блоков. Нет стандартной схемы заполнения блоков, и есть из чего выбирать. LockBox не использует заполнение блоков для CBC (другие режимы могут быть другим случаем). Если открытый текст представляет собой целое число блоков, количественное определение не требуется или не производится, в противном случае используется стандартное кража CipherText. Если размер открытого текста очень мал (от 1 до 15 байт), кража зашифрованного текста невозможна, и вместо этого используется схема заполнения. Чтобы обеспечить взаимодействие в отношении квантования блоков, вы можете выбрать следующие варианты:
4.1 Проверьте свою документацию на наличие иностранного кодека в отношении квантования блоков (он может быть включен в раздел "заполнение сообщениями"). Если иностранный кодек использует кражу зашифрованного текста, значит, вы не против (просто убедитесь, что нет коротких сообщений).
4.2 В противном случае вы можете сделать свой собственный отступ. Со стороны lockbox lockbox ничего не делает с сообщениями, которые уже находятся в целых блоках. Очень вероятно, что иностранный кодек имеет ту же политику - но опять же вам нужно проверить документацию для иностранного кодека.
Мне удалось успешно реализовать код Трои Delphi в 10.2 Токио с несколькими изменениями.
Я удалил TNTLxUtils из использования, поскольку он не был нужен (и у меня его не было), и добавил IdGlobal. Причина использования IdGlobal заключается в том, что вам необходимо преобразовать тип TBytes в TIdBytes в функции Base64_Encode и вернуть TIBytes обратно в TBytes в Base64_Decode.
Примечание. Этот модуль будет работать только в 32-разрядных приложениях, поскольку он ссылается на 32-разрядный API-интерфейс Windows.
Спасибо, Трой, за то, что указал мне правильное направление для бесплатного метода шифрования, который не требует покупки инструментария для реализации.
unit CryptoUtils;
interface
function AES128_Encrypt(Value, Password: string): string;
function AES128_Decrypt(Value, Password: string): string;
implementation
uses
SysUtils, Windows, IdCoderMIME, IdGlobal;
//-------------------------------------------------------------------------------------------------------------------------
// Base64 Encode/Decode
//-------------------------------------------------------------------------------------------------------------------------
function Base64_Encode(Value: TBytes): string;
var
Encoder: TIdEncoderMIME;
begin
Encoder := TIdEncoderMIME.Create(nil);
try
Result := Encoder.EncodeBytes(TIdBytes(Value));
finally
Encoder.Free;
end;
end;
function Base64_Decode(Value: string): TBytes;
var
Encoder: TIdDecoderMIME;
begin
Encoder := TIdDecoderMIME.Create(nil);
try
Result := TBytes(Encoder.DecodeBytes(Value));
finally
Encoder.Free;
end;
end;
//-------------------------------------------------------------------------------------------------------------------------
// WinCrypt.h
//-------------------------------------------------------------------------------------------------------------------------
type
HCRYPTPROV = Cardinal;
HCRYPTKEY = Cardinal;
ALG_ID = Cardinal;
HCRYPTHASH = Cardinal;
const
_lib_ADVAPI32 = 'ADVAPI32.dll';
CALG_SHA_256 = 32780;
CALG_AES_128 = 26126;
CRYPT_NEWKEYSET = $00000008;
PROV_RSA_AES = 24;
KP_MODE = 4;
CRYPT_MODE_CBC = 1;
function CryptAcquireContext(var Prov: HCRYPTPROV; Container: PChar; Provider: PChar; ProvType: LongWord; Flags: LongWord): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptAcquireContextW';
function CryptDeriveKey(Prov: HCRYPTPROV; Algid: ALG_ID; BaseData: HCRYPTHASH; Flags: LongWord; var Key: HCRYPTKEY): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDeriveKey';
function CryptSetKeyParam(hKey: HCRYPTKEY; dwParam: LongInt; pbData: PBYTE; dwFlags: LongInt): LongBool stdcall; stdcall; external _lib_ADVAPI32 name 'CryptSetKeyParam';
function CryptEncrypt(Key: HCRYPTKEY; Hash: HCRYPTHASH; Final: LongBool; Flags: LongWord; pbData: PBYTE; var Len: LongInt; BufLen: LongInt): LongBool;stdcall;external _lib_ADVAPI32 name 'CryptEncrypt';
function CryptDecrypt(Key: HCRYPTKEY; Hash: HCRYPTHASH; Final: LongBool; Flags: LongWord; pbData: PBYTE; var Len: LongInt): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDecrypt';
function CryptCreateHash(Prov: HCRYPTPROV; Algid: ALG_ID; Key: HCRYPTKEY; Flags: LongWord; var Hash: HCRYPTHASH): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptCreateHash';
function CryptHashData(Hash: HCRYPTHASH; Data: PChar; DataLen: LongWord; Flags: LongWord): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptHashData';
function CryptReleaseContext(hProv: HCRYPTPROV; dwFlags: LongWord): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptReleaseContext';
function CryptDestroyHash(hHash: HCRYPTHASH): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDestroyHash';
function CryptDestroyKey(hKey: HCRYPTKEY): LongBool; stdcall; external _lib_ADVAPI32 name 'CryptDestroyKey';
//-------------------------------------------------------------------------------------------------------------------------
{$WARN SYMBOL_PLATFORM OFF}
function __CryptAcquireContext(ProviderType: Integer): HCRYPTPROV;
begin
if (not CryptAcquireContext(Result, nil, nil, ProviderType, 0)) then
begin
if HRESULT(GetLastError) = NTE_BAD_KEYSET then
Win32Check(CryptAcquireContext(Result, nil, nil, ProviderType, CRYPT_NEWKEYSET))
else
RaiseLastOSError;
end;
end;
function __AES128_DeriveKeyFromPassword(m_hProv: HCRYPTPROV; Password: string): HCRYPTKEY;
var
hHash: HCRYPTHASH;
Mode: DWORD;
begin
Win32Check(CryptCreateHash(m_hProv, CALG_SHA_256, 0, 0, hHash));
try
Win32Check(CryptHashData(hHash, PChar(Password), Length(Password) * SizeOf(Char), 0));
Win32Check(CryptDeriveKey(m_hProv, CALG_AES_128, hHash, 0, Result));
// Wine uses a different default mode of CRYPT_MODE_EBC
Mode := CRYPT_MODE_CBC;
Win32Check(CryptSetKeyParam(Result, KP_MODE, Pointer(@Mode), 0));
finally
CryptDestroyHash(hHash);
end;
end;
function AES128_Encrypt(Value, Password: string): string;
var
hCProv: HCRYPTPROV;
hKey: HCRYPTKEY;
lul_datalen: Integer;
lul_buflen: Integer;
Buffer: TBytes;
begin
Assert(Password <> '');
if (Value = '') then
Result := ''
else begin
hCProv := __CryptAcquireContext(PROV_RSA_AES);
try
hKey := __AES128_DeriveKeyFromPassword(hCProv, Password);
try
// allocate buffer space
lul_datalen := Length(Value) * SizeOf(Char);
Buffer := TEncoding.Unicode.GetBytes(Value + ' ');
lul_buflen := Length(Buffer);
// encrypt to buffer
Win32Check(CryptEncrypt(hKey, 0, True, 0, @Buffer[0], lul_datalen, lul_buflen));
SetLength(Buffer, lul_datalen);
// base 64 result
Result := Base64_Encode(Buffer);
finally
CryptDestroyKey(hKey);
end;
finally
CryptReleaseContext(hCProv, 0);
end;
end;
end;
function AES128_Decrypt(Value, Password: string): string;
var
hCProv: HCRYPTPROV;
hKey: HCRYPTKEY;
lul_datalen: Integer;
Buffer: TBytes;
begin
Assert(Password <> '');
if Value = '' then
Result := ''
else begin
hCProv := __CryptAcquireContext(PROV_RSA_AES);
try
hKey := __AES128_DeriveKeyFromPassword(hCProv, Password);
try
// decode base64
Buffer := Base64_Decode(Value);
// allocate buffer space
lul_datalen := Length(Buffer);
// decrypt buffer to to string
Win32Check(CryptDecrypt(hKey, 0, True, 0, @Buffer[0], lul_datalen));
Result := TEncoding.Unicode.GetString(Buffer, 0, lul_datalen);
finally
CryptDestroyKey(hKey);
end;
finally
CryptReleaseContext(hCProv, 0);
end;
end;
end;
end.
- Не используйте LockBox 3. Это не очень качественная библиотека.
- Не возвращайте зашифрованные данные в "текстовые" строки. Зашифрованные данные - это произвольные последовательности байтов, а не строки (как текстовые данные). Delphi использует строки с контролем длины и может хранить практически все, что угодно, но может, но вы можете столкнуться с проблемами передачи строк, содержащих последовательности байтов, которые могут быть неверно интерпретированы другими языками, например, $00 приложением C/C++..). Если сама библиотека использует строки, то это признак того, что это некачественная библиотека
- Не преобразовывайте зашифрованные данные! Когда вы конвертируете зашифрованный ANSIString в Unicode (я думаю, это и есть причина вашего последнего приведения), вы уничтожаете зашифрованное значение. Если вы передадите эту строку, она не будет расшифрована, если не будет применено обратное преобразование, если только она не "с потерями".
У меня просто была такая же проблема. Я знаю, что это старая тема, но она мне очень помогла. Я просто оставляю это здесь для записи.
Function LockBoxDecrypt(Password As String, Data() As Byte) As String
Dim AesProvider = AesCryptoServiceProvider.Create()
Dim IV(15) As Byte, PaddedData(15) As Byte
Array.Copy(Data, 0, IV, 0, 8)
Array.Copy(Data, 8, PaddedData, 0, Data.Length - 8)
AesProvider.Key = SHA1.Create().ComputeHash(Encoding.Default.GetBytes(Password)).Take(16).ToArray()
AesProvider.IV = IV
AesProvider.Mode = CipherMode.CFB
AesProvider.Padding = PaddingMode.None
Return Encoding.Default.GetString(AesProvider.CreateDecryptor().TransformFinalBlock(PaddedData, 0, PaddedData.Length), 0, Data.Length - 8)
End Function
Основываясь на ответе Шона, я предполагаю, что режим должен быть изменен на CTS при наличии более 1 блока. Я не пробовал, потому что мне достаточно 1 блока, но адаптировать код должно быть легко.