Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to AES-128 encrypt a string using a password in Delphi and decrypt in C#?

I'd like to AES-128 encrypt a string in Delphi with a password. I'd like to upload this to my server and be able to decrypt given the same password in C#.

In Delphi, I'm using 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;

How can I decrypt the resulting string in C#? I can change the Delphi code. Nothing is in production yet. I'm not even stuck on using LockBox. But, I would like to avoid putting this in a DLL for P/Invoke.

(My example shows that my encrypted sequence is itself a string. This is not a requirement for me. A stream of bytes is fine.)

like image 573
Troy Avatar asked Nov 27 '22 01:11

Troy


2 Answers

I finally found a compatible solution between Delphi and C# for AES-128. It's also works on Wine. Here's my Delphi code:

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.

And here's my C# code:

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);
        }
    }
}
like image 85
Troy Avatar answered Dec 31 '22 12:12

Troy


Contrary to any troll flame-bait that you might read, LockBox 3 is actually a good quality cryptographic library. The standards compliance of LB3 is impecable. Where you might have problems with interoperability with other languages & libraries is in relation to options that are outside of the standard. If using Lockbox on the Delphi side, then you just need to make sure that these options are handled the same way on the other language's side. If this is not possible, then you should choose another library. I will deal with each of these options below.

There is nothing wrong with the alternative solutions (OpenSSL, CryptoAPI and Eldos). Some of them may be black-box. This might be an issue for some peoople.

  1. Converting password to key. AES-128 uses a 16 byte key. Also the standard mechanism to generate a key from "key data" or "password data" is natively based on a 16 byte input seed. It is safer for interoperability to generate the binary key from the string password on the Delphi side, and just transport the binary key to the other side, rather than transport the string password. This is because the algorithm to convert a string password to a binary 16-byte key is outside the AES standard. Nether-the-less, you can do it either way. When lockbox is given a string password to initialize an AES-128 codec, it looks at the string payload as an array of bytes. If the payload is precisely 16 bytes, then great, it can be passed directly to the AES key generation algorithm, which is specified in the standard. If the string payload is not precisely 16 bytes, then payload will be hashed with SHA-1 to produce a 20 byte hash output. The low 16 bytes of this hash are then passed to the standard AES key generation function. So, your options for ensuring interoperability in relation to key initialization are:

    1.1. Transport binary keys instead of string passwords.

    1.2. If Option 1.2 is too inconvenient, then transport the password, but mimic the same password-to-key algorithm on the other side.

    1.3. If 1 & 2 are not working for some reason, try to restrict passwords to exactly 16 bytes (8 UTF-8 characters or 16 UTF-16 code-points). This should be pretty safe if the other language's implementation is half decent.

  2. UTF-16 versus ansi-string/UTF-8 passwords This is not so much an option, but a trap for young players. We programmers tend to think of "strings" as "strings". But it is not so. In Delphi 2010, the payload of strings are stored in a UTF-16LE encoding with a code-unit size of 2 bytes. But in other languages, such as PHP and python, in the default mode, strings are single-byte code-unit encodings, either UTF-8 or something based on an MS windows code-page base (which MS calls "ansistring"). It pays to remember than UTF-16 encoding of 'mypassword' is not the same as UTF-8 'mypassword'.

  3. IV setup. The AES standard does not deal with the question of how to set up the codec' Initialization Vector (IV). The size of the IV is the same as the size of the underlying block. For AES this is 128 bits or 16 bytes. When encrypting, lockbox creates a 16 byte nonce. This nonce becomes the value of the IV, and it is emitted in the clear at the head of the ciphertext message. Read the documentation on the other side's method/policy for IV initialization. Your options are:

    3.1 If the other side prepends the IV to the ciphertext, then you are sweet.

    3.2 Otherwise, on the other side, when decrypting, read the first 16 bytes of the ciphertext yourself, and pass the remainder to the foreign codec. Before decryption, tell you foreign codec what the IV is (assuming it's API is capable of this).

  4. Block quantisation The AES block size is 16 bytes. When the plaintext message is not precisely a whole multiple 16 bytes, something must be done to make it a whole multiple. This procedure is called block quantisation and is not dealt with in the standard, but left up to the implementation. Many implementations will use block padding. There is no standard block padding scheme and there are many to choose from. LockBox does not use block padding for CBC (other modes may be a different case). If the plaintext is a whole number of blocks, no quantisation is needed or done, otherwise standard CipherText stealing is used. If the plaintext size is very small (between 1 and 15 bytes) ciphertext stealing is not possible, and a padding scheme is used instead. To ensure interoperability in relation to block quantisation, your options are:

    4.1 Check your documentation for the foreign codec in relation to block quantisation (it may come under the heading of "message padding"). If the foreign codec uses ciphertext stealing, then you are sweet (just make sure no short messages).

    4.2 Otherwise you could do your own padding. On the lockbox side, lockbox does nothing to messages that are already in whole blocks. Very probably the foreign codec has the same policy - but again you need to check the documentation for the foreign codec.

like image 39
Sean B. Durkin Avatar answered Dec 31 '22 14:12

Sean B. Durkin