1

I have a client providing an API that dictates the data to send to them must be encrypted with AES, 128-bit key, ECB mode, and PKCS5Padding. I'm trying to use LockBox 3 in Delphi 10.3 Rio and am not getting the same encrypted string as an online test tool they pointed to for verification. It is close, but not quite there.

With lots of reading here about Unicode, PKCS5Padding, and related questions, I've come to the end of what to try. I must admit I don't do very much with encryption and have been reading as much as I can to get my head around this before I ask questions.

There are a couple things I'd like confirmation on:

  • The difference between a password and a key. I've read that LB3 uses a password to generate a key, but I have specific instructions from the client on how the key is to be generated so I've made my own Base64-encoded key and am calling InitFromStream to initialize it. I believe this takes the place of setting a password, is that right? Or maybe passwords are only used by Asymetric cipers (not Symetric ones, like AES)?
  • PKCS5Padding: I was worried by something I read on the LB3 Help site that said padding is done intelligently depending on the choice of cipher, chaining mode, etc. So does that mean there's no way to force it to use a specific padding method? I've converted the data to a byte array and implemented by own PKCS5Padding but I think LB3 may still be padding beyond that. (I've tried looking through the code and have not found any evidence this is what it's doing.)

Should I use a different encryption library in Delphi to accomplish this? I've checked out DelphiEncryptionCompendium and DcPCryptV2 but I found LB3 seems to have the most support and I felt it was the easiest to work with, especially in my Unicode version of Delphi. Plus I have used LockBox 2 quite a bit in years past, so I figured it would be more familiar (this turned out not to be the case).

To illustrate what I've tried, I extracted my code from the project to a console application. Perhaps my assumptions above are correct and there's a glaring error in my code or a LB3 parameter I don't understand that someone will point out:

program LB3ConsoleTest;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Classes, System.NetEncoding,
  uTPLb_Codec, uTPLb_CryptographicLibrary,
  uTPLb_StreamUtils, uTPLb_Constants;

var
  Codec: TCodec;
  CryptographicLibrary: TCryptographicLibrary;

function PKCS5PadStringToBytes(RawData: string; const PadSize: Integer): TBytes;
{ implement our own block padding }
var
  DataLen: Integer;
  PKCS5PaddingCount: ShortInt;
begin
  Result := TEncoding.UTF8.GetBytes(RawData);
  DataLen := Length(RawData);

  PKCS5PaddingCount := PadSize - DataLen mod PadSize;
  if PKCS5PaddingCount = 0 then
    PKCS5PaddingCount := PadSize;
  Inc(DataLen, PKCS5PaddingCount);

  SetLength(Result, DataLen);
  FillChar(Result[DataLen - PKCS5PaddingCount], PKCS5PaddingCount, PKCS5PaddingCount);
end;

procedure InitializeAESKey(const AESKey: string);
{ convert the string to a byte array,
  use that to initialize a ByteStream,
  and call LB3's InitFromStream }
var
  AESKeyBytes: TBytes;
  AESKeyStream: TBytesStream;
begin
  AESKeyBytes := TEncoding.UTF8.GetBytes(AESKey);
  AESKeyStream := TBytesStream.Create(AESKeyBytes);
  Codec.InitFromStream(AESKeyStream);
end;

const
  RawData = '{"invoice_id":"456456000018047","clerk_id":"0023000130234234","trans_amount":1150034534,"cust_code":"19455605000987890641","trans_type":"TYPE1"}';
  AESKeyStr = 'CEAA31AD1EE4BDC8';
var
  DataBytes: TBytes;
  DataStream: TBytesStream;
  ResultStream: TBytesStream;
  ResultBytes: TBytes;
  Base64Encoder: TBase64Encoding;
begin
  // create the LockBox3 objects
  Codec := TCodec.Create(nil);
  CryptographicLibrary := TCryptographicLibrary.Create(nil);
  try
    // setup LB3 for AES, 128-bit key, ECB
    Codec.CryptoLibrary := CryptographicLibrary;
    Codec.StreamCipherId := uTPLb_Constants.BlockCipher_ProgId;
    Codec.BlockCipherId  := Format(uTPLb_Constants.AES_ProgId, [128]);
    Codec.ChainModeId    := uTPLb_Constants.ECB_ProgId;

    // prep the data, the key, and the resulting stream
    DataBytes := PKCS5PadStringToBytes(RawData, 8);
    DataStream := TBytesStream.Create(DataBytes);
    InitializeAESKey(AESKeyStr);
    ResultStream := TBytesStream.Create;

    // ENCRYPT!
    Codec.EncryptStream(DataStream, ResultStream);

    // take the result stream, convert it to a byte array
    ResultStream.Seek(0, soFromBeginning);
    ResultBytes := Stream_to_Bytes(ResultStream);

    // convert the byte array to a Base64-encoded string and display
    Base64Encoder := TBase64Encoding.Create(0);
    Writeln(Base64Encoder.EncodeBytesToString(ResultBytes));

    Readln;
  finally
    Codec.Free;
    CryptographicLibrary.Free;
  end;
end.

This program generates an encrypted string 216 characters long with only the last 25 different than what the online tool produces.

Why?

David Cornelius
  • 437
  • 1
  • 5
  • 12
  • This looks a little, er, suspicious: `FillChar(Result[DataLen - PKCS5PaddingCount], PKCS5PaddingCount, PKCS5PaddingCount);`. Are you sure that is correct? What is the value of PKCS5PaddingCount? – Rudy Velthuis Mar 13 '19 at 10:15
  • FWIW, when migrating from Delphi 2007 to (I think) XE5 at the time, I had issues with LockBox as well, and stuff I encryped before couldn't be decrypted with the newer version. I ended up compiling a separate dll with the 2007 version of the decryption logic, so I could decrypt it in the XE5 build of my application. – GolezTrol Mar 13 '19 at 10:55
  • IIRC PKCS5 padding is about 8 bytes blocks, whereas AES uses 16 bytes blocks. So I guess you meant PKCS7 padding, not PKCS5 padding. – Arnaud Bouchez Mar 13 '19 at 11:37
  • Also note that ECB chaining mode is pretty unsafe, and should be avoided at all cost. Worth changing to a new mode (and 256-bit?). – Arnaud Bouchez Mar 13 '19 at 11:39
  • 1
    For a cross-platform (Delphi + FPC), well maintained Open Source alternative with very fast process (the fastest for sure since it is the only one using AES-NI), consider our https://github.com/synopse/mORMot/blob/master/SynCrypto.pas – Arnaud Bouchez Mar 13 '19 at 11:40
  • @RudyVelthuis This is how PKCS5/PKCS7 work: pad with the number of padding bytes to match the whole encryption block size. – Arnaud Bouchez Mar 13 '19 at 13:49
  • @Arnaud: Ok, thanks. So I gather this is correct and even required. – Rudy Velthuis Mar 13 '19 at 13:51
  • @RudyVelthuis, Yes, I'm sure the FillChar is right. I've stepped through it in code and it adds and fills the correct bytes. Reading about PKCS5Padding surprised me in that the bytes are filled with the byte number of bytes filled (follow that?). – David Cornelius Mar 13 '19 at 14:57
  • @GolezTrol Interesting approach--calling a DLL made with an earlier version of Delphi. I may try that. I've also considered using an external, third-party DLL, possibly even commercial. – David Cornelius Mar 13 '19 at 14:59
  • @David: yes, Arnaud already told me. – Rudy Velthuis Mar 13 '19 at 16:51

1 Answers1

1

AES uses 16-bytes blocks, not 8-bytes.

So you need PKCS7 with 16 bytes padding, not PKCS5 which is fixed to 8 bytes.

Please try

DataBytes := PKCS5PadStringToBytes(RawData, 16);

Also consider changing the chaining mode away from ECB, which is pretty weak, so is to be avoided for any serious work.

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • From my understanding, AES can use either 8-byte blocks or 16-byte blocks. The standard (and possibly default in many libraries) is 16-byte blocks but the API I am calling specifically uses PKCS5Padding. It also specifies ECB mode, another requirement (as mentioned in the original post). The weakness of this mode is, I suppose, accounted for by dynamically generating the AES key out of the data which includes a one-time-use customer authorization code. – David Cornelius Mar 13 '19 at 15:09
  • AES works on 16 bytes blocks - I don't know what you are referring to. The API you are calling is misleading, for sure. PKCS5 is for 8 bytes padding only: allowing a parameter for padding size is what PKCS7 is for - not PKCS5. And ECB should *not* be used, even with an ephemeral key. If the user can guess only some uncyphered bytes (e.g. the first 16 bytes), it would allow to decypher the whole content. – Arnaud Bouchez Mar 13 '19 at 21:15
  • To be honest, I don't care what the padding is--I just want my encrypted string to be able to be decrypted by the 3rd-party API on the other end. And to do that, I have to be able to match my output with that of the online tool, https://www.devglan.com/online-tools/aes-encryption-decryption. The reason I keep mentioning PKCS5Padding is that's what the API documentation requires--that and ECB. Doesn't matter how insecure or outdated or whatever, it's what I have to produce to finish this project. – David Cornelius Mar 13 '19 at 21:38
  • So did changing to `RawData, 16` fix your problem? – Arnaud Bouchez Mar 14 '19 at 16:14
  • No, I had already tried that before posting the question. I did find one thing odd/interesting: using 8-byte padding returned the right length of string, 16-byte padding returned a string too long, but if I cut it off at the length the 8-byte padded string resulted in, the encrypted result matched! However, I needed to add another parameter to RawData and now can't find a similar pattern. That "hack" would've worked otherwise. – David Cornelius Mar 14 '19 at 17:09