4

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.)

Troy
  • 1,237
  • 2
  • 13
  • 27
  • Its not really clear what your question is. – BillRobertson42 Feb 08 '12 at 04:58
  • Thanks, Bill. I edited the question to make it more clear. – Troy Feb 08 '12 at 05:06
  • Delphi and LockBox seem to have little to do with your C# question. Focus on AES and all related details in the cryptology parlance. – menjaraz Feb 08 '12 at 06:37
  • 3
    But my question is "How can I AES-encrypt a string in Delphi and then decrypt it in C#?". How is Delph irrelevant to my question? I offer my code as an example of how I'd like to encrypt in Delphi. But I'm not stuck on using Delphi. If someone could show how you can get the job done some other way, I'm all ears. – Troy Feb 08 '12 at 13:39

5 Answers5

17

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);
        }
    }
}
Troy
  • 1,237
  • 2
  • 13
  • 27
  • 1
    I haven't got the time to review all the code, but I do see that you're using a static IV set to all zero's. It should be pretty easy to use a random IV and prepend that to the cipher text before encoding it in base 64. Also note that an IV is not KEY_SIZE bytes in size, but BlockSize in bytes (in this example they are fortunately equal). Most of the time you can ask the algorithm instance for the block size. Finally you might want to use UTF8 instead of UNICODE, especially since it is impossible to compress ciphertext afterwards (you will likely have 2 times less ciphertext to worry about). – Maarten Bodewes Feb 15 '12 at 22:57
9

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.

Sean B. Durkin
  • 12,659
  • 1
  • 36
  • 65
  • 1
    Excellent answer. I've also just encountered a situation where I need to encrypt something that another tool can decrypt. We can choose the decryption tool, but it has to be something standard and easily obtainable, so we're going with OpenSSL. I'm in the process of learning whether LB3 can encrypt something in AES that OpenSSL can decrypt. This answer gives me a lot of knowledge that I didn't have previously. – Jon Robertson Feb 09 '12 at 01:06
  • 1
    Thank you so much, Sean, for a really constructive answer. It sort of confirms my suspicion that the real challenge is converting the password into a 128-bit key consistently between platforms. Your information helps me on my journey of figuring this out. There's so much to this encryption stuff, it's hard to keep straight. I wish there was a simple solution to this all! Thanks for helping me get on the right path. Now, if someone could just point me to a code sample that demonstrates it! ;) – Troy Feb 09 '12 at 03:43
  • LOL! You're the author of that library, so it's not surprise you think is "good quality". Users may think very differently. Moreover, you had stolen the name and even attempted to change the license when you had no right to do it - I would not trust any "security" code from someone who doesn't care at all about someone else rights. –  Feb 09 '12 at 13:15
  • Sean, your information is great. But after an hour of weeding through the gory details, I gave up. I guess I'm a mere mortal when it comes to cryptography. I decided to install Delphi XE2 and compile a x64 dll with my 2 functions (encrypt/decrypt a string given a password). Then I thought, maybe there's a simple dll out there that has these 2 simple functions, is of a good quality, and has a 32-bit and 64-bit version. Do you know of such a thing? – Troy Feb 09 '12 at 15:43
  • ldsandon, if you don't think LockBox 3 is of good quality, can you tell me specifically what the problem is? For the time being, it's in my first place. I noticed that they have unit tests in place. Perhaps the problems you experienced have been resolved. – Troy Feb 09 '12 at 15:49
  • You can make a search here too and find what people found about Lockbox 3. Maybe the problems ahve been resolved, but I switched to libraries which gave me no problems. I have to talk to many different external systems and I really have no time to debug a library. –  Feb 09 '12 at 20:06
  • Troy, if you don't need interoperability with C#, then LB3 should be the simplest solution. If you require interoperability with C#, then for now, go with OpenSSL or MS CryptAPI. I will look at extending LB3's OpenSLL wrapping to include encrypt/decrypt, but that might take a couple of weeks. Right now, LB3 only surfaces the sign/verify functionality of OpenSSL. – Sean B. Durkin Feb 09 '12 at 23:19
  • Oh - and one other little thought. If you can control both sides, I suggest choosing a key-streaming mode rather than CBC. With key-streaming modes wuch as CFB, block quantisation becomes a non-issue as the last block can be safely truncated. This is better for cross-platform interoperability. – Sean B. Durkin Feb 09 '12 at 23:26
  • I looked at OpenSSL, found a Delphi wrapper, but got stuck on the 20 things I had to learn to get the job done. No examples (in Delphi) anywhere on how to do it. I'm leaning towards making my own dll w/ LB3, and using XE2 to make a 64-bit version. But I'm still looking for an off-the-shelf dll that I can p/invoke from C# and use from Delphi that is simple and gets the job done. I'd prefer one function call to encrypt, and one function call to decrypt! I'm hoping to find one that has a good reputation, and comes in both 32-bit and 64-bit. Know of any? – Troy Feb 09 '12 at 23:27
  • Yeah, I sympathise. OpenSSL can be really hard. I don't know much about C#. Can C# invoke a delphi-built DLL? If so this might be the solution. Lockbox3, by the way, can target both 32 and 64 bit. – Sean B. Durkin Feb 09 '12 at 23:30
  • I believe C# can invoke any DLL via P/Invoke. – Troy Feb 13 '12 at 22:14
  • If you want to try the DLL solution and need some help, make a (Delphi) DLL with the API as you like it, and I will write the implementation for you, so you can encrypt from the Delphi side, and decrypt from the C# side. – Sean B. Durkin Feb 13 '12 at 23:07
  • Could you please state your affiliation to LockBox 3 in this answer? I still haven't found any official documentation (API or otherwise) for the library, issues go unanswered on GitHub. Do you still support LockBox or can we by now assume that it has failed to gain traction? – Maarten Bodewes Jan 09 '19 at 14:46
  • and a few years later there's still no sample code or documentation as to how this "answer" is supposed to work. What's 20 lines of C++ or C# is a maze of weird lockbox guesswork and Sean has apparently lost interest. – Code Abominator Oct 26 '22 at 22:37
7

I was able to successfully implement Troy's Delphi code in 10.2 Tokyo with a couple of modifications.

I removed TNTLxUtils from the Uses as it was not needed (and I didn't have it) and added IdGlobal. The reason for using IdGlobal is that you need to convert the type TBytes to TIdBytes in the Base64_Encode function and TIBytes back to TBytes in Base64_Decode.

Note: This unit will only work in 32-bit applications as it references the 32-bit Windows API.

Thanks, Troy for pointing me in the right direction for a free method of encryption that doesn't require purchasing a toolkit to implement.

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.
Brad Weaver
  • 71
  • 1
  • 3
  • 1
    Hi Brad, I've just tried your code in Delphi XE8 on my 32bit App but it generates an error when attempting to decrypt the string; System Error. Code: -2146893819. Bad Data. – Jerry Mallett May 02 '21 at 11:57
  • Sadly now outdated with the switch to CNG. Give it time, MS will remove this DLL entirely :) – Code Abominator Oct 26 '22 at 22:48
5
  • Don't use LockBox 3. It's not a good quality library.
  • Do no return encrypted data into "text" strings. Encrypted data are arbitrary sequences of bytes, not strings (as textual data). Delphi uses "length controlled" strings and can store almost anything it hem, but may but you may encounter issues passing around strings that contain byte sequences that could be interpreted the wrong way by other languages, i.e. $00 by a C/C++ application...). If the library itself uses strings, well, it's a symptom it's a low quality library
  • Do not transform encrypted data! When you convert your encrypted ANSIString into a Unicode one (I guess that's the reason of your last cast), you're destroying the encrypted value. If you pass that string around, it won't be decryptable unless the reverse transformation is applied, as long as it is not "lossy".
  • +1 Don't know about LockBox' quality, but using strings for byte sequences was fairly normal in pre-Unicode Delphi. I guess because of the easy indexing and the "free" memory management. It "only" became a real problem with Unicode Delphi because of the: implicit and explicit string conversions. – Marjan Venema Feb 08 '12 at 09:58
  • Like I said, I'm not stuck on using Lockbox. But what would you suggest I use in its place? I edited my question to show that I'm not stuck on using a string to store the encrypted data. – Troy Feb 08 '12 at 13:42
  • The conversion in my example from AnsiString to Unicode is on a base64 encoded string. So there's no chance of data loss. But again, my point isn't my example. My example is just showing a possible starting point. – Troy Feb 08 '12 at 13:44
  • @Troy: one option is to use Windows CryptoAPI (which will be probably called by C# as well). Another options is OpenSSL. For Delphi libraries you can look at Eldos SecureBlackBox or if you'd like a free one http://code.google.com/p/delphidec/ –  Feb 08 '12 at 16:57
  • That's fair. I downvoted your answer because it only critiqued my example, which was not the point of my question. I appreciate your trying to help, and I'm not feeling the need for revenge. I just found your answer not helpful, which is what a down vote means. Nothing more is intended. Sorry if you felt otherwise. – Troy Feb 09 '12 at 15:46
  • ldsandon, I'm now downvoting because of your rude comment. It seems obvious you accused Sean of downvoting. I've read several of your answers and comments. I find most of them to be unhelpful. Sean, on the other hand, posted a thoughtful and helpful answer. Your personal opinion of Sean or his actions does not answer the OP's question. – Jon Robertson Feb 09 '12 at 19:40
  • @Troy: if you're not ready to sustain critiques you'll never become a good programmer. I pointed out issues within your code that could make the encrypted data unreadable by another application written in a different language, as far as I could see from the code you posted. Were them wrong? No. If you expected a ready made solution, you should try rent-a-coder instead. Because AES is a standard algorithm usually issues are in user code, or bugs in libraries. That's why important to use well tested libraries with good support. And in security, that's more important than with anything else. –  Feb 09 '12 at 19:53
  • Ok, I see people here use downvotes as their personal revenge method. Stackoverflow is getting a bad place really. I didn't accuse anyone of downvoting - just said that when downvoting people can explain why - and downvotes should be based on answer technical merits, not what you think about me. You say my answers are unhelpful, but given my reputation is a bit higher than yours it looks they are not. I don't answer here to code on someone else behalf. My answer usually point to what I believe it's the right direction, then it's up to the developer to use his own brain. –  Feb 09 '12 at 20:02
  • I downvoted because your answer was unhelpful to answering my specific question. I didn't take it as critique, since I just cobbled together the example code without really understanding it. That's why I was aking guidance from the community on StackOverflow. I mean nothing personal by the downvote. And I appreciate the information ... in case I decide to commit to my example code. – Troy Feb 09 '12 at 20:30
  • @Troy: you should read the Stackoverflow FAQ about downvoting. I could have downvoted your question because it was unclear, and you didn't explain what is wrong between your Delphi and C# code. For example, where did you get stuck? Did you got error messages? If so, what? But instead downvoting, because you look a beginner - at least about encryption - I just pointed out what could cause issues in the only code you submitted. I could write the whole code for you, but should I? You won't get much help if you downvote whoever tries to help you unless he gives you the whole answer ready to use –  Feb 09 '12 at 22:09
  • Fine. I removed my downvote. I guess I can be happy with being neutral. :) – Troy Feb 09 '12 at 23:29
  • How about LockBox 10? They still claim to support AES, but it's nowhere in their library. Also, there is literally no documentation. And, I'm having bugs where decryption works only half the time (decrypt works every other time, one good one bad one good one bad...) – Jerry Dodge Jan 10 '15 at 00:58
-1

I just had the same problem. I know this is an old topic, but it helped me a lot. I'm just leaving it here for the record.

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

Based on Sean's answer, I assume that the mode should be changed to CTS when having more than 1 block. I didn't try it, because 1 block is enough for me, but it should be easy to adapt the code.

Leonardo
  • 124
  • 1
  • 8