Adrien Reboisson
Adrien Reboisson

Reputation: 631

DCPCrypt (Delphi) + ZLib / "data error" when decompressing decrypted data

I designed a system that stores blocks of data, sent compressed and encrypted by multiple clients.

When trying to fetch stored data blocks (that I need to decompress and decrypt) on a specific computer, a ZLib "data error" exception is raised during the data decompression. It seems clear that the stream has not been properly decrypted so the input stream is not a valid ZLib stream, leading to a such issue.

After some research I discovered that for an identical block of source data that is also stored, this problematic block contained totaly different data, so it's quite clear that there is an issue in the encryption or the compression algorithm.

However, the fact that I cannot easily reproduce the problem means that if there is an error it's something far from being obvious. So I'm posting the code here, hoping someone will spot something I didn't see.

First, the data block AStream is compressed :

function CompressStream(AStream: TMemoryStream;
  ACompressionLevel:
  TCompressionLevel): Boolean;
var
  LTempStream: TMemoryStream;
  LCompressedStream: TCompressionStream;
begin
  LTempStream := TMemoryStream.create;
  try
    try
      AStream.Seek(0, soBeginning);
      LCompressedStream := TCompressionStream.Create(ACompressionLevel, LTempStream);
      try
        try
          LCompressedStream.CopyFrom(AStream, AStream.Size);
        finally
          LCompressedStream.free;
        end;
      finally
        AStream.Clear;
        AStream.CopyFrom(LTempStream, 0);
        AStream.Seek(0, soBeginning);
        Result := True;
      end;
    finally
      LTempStream.free;
    end;
  except
    Result := False;
  end;
end;

Then, it's encrypted :

function EncryptStream(AStream: TMemoryStream; const AParameters: AnsiString): Boolean;
var
  LModifiedStream: TMemoryStream;
  LRijndaelCipher: TDCP_rijndael;
begin
   try
     LRijndaelCipher := TDCP_rijndael.Create(nil);
     LModifiedStream := TMemoryStream.Create;
     InitCipherWithKey(LRijndaelCipher, AParameters);
     try
        AStream.Seek(0, soBeginning);
        LRijndaelCipher.EncryptStream(AStream, LModifiedStream, AStream.Size);
        TMemoryStream(AStream).Clear;
        AStream.CopyFrom(LModifiedStream, 0);
        Result := True;
     finally
        LRijndaelCipher.Burn;
        LRijndaelCipher.Free;
        LModifiedStream.Free;
     end;
   except
     Result := False;
   end;
end;

...and stored somewhere.

When fetching a data block, it's first decrypted :

function DecryptStream(AStream: TMemoryStream; const AParameters: AnsiString): Boolean;
var
  LModifiedStream: TMemoryStream;
  LRijndaelCipher: TDCP_rijndael;
begin
   try
     LRijndaelCipher := TDCP_rijndael.Create(nil);
     LModifiedStream := TMemoryStream.Create;
     InitCipherWithKey(LRijndaelCipher, AParameters);
     try
        AStream.Seek(0, soBeginning);
        LRijndaelCipher.DecryptStream(AStream, LModifiedStream, AStream.Size);
        TMemoryStream(AStream).Clear;
        AStream.CopyFrom(LModifiedStream, 0);
        Result := True;
     finally
        LRijndaelCipher.Burn;
        LRijndaelCipher.Free;
        LModifiedStream.Free;
     end;
   except
     Result := False;
   end;
end;

Then uncompressed :

function DecompressStream(AStream: TMemoryStream): Boolean;
var
  LTempStream: TMemoryStream;
  LCompressedStream: TDecompressionStream;
begin
  LTempStream := TMemoryStream.create;
  try
    try
      AStream.Seek(0, soBeginning);
      LCompressedStream := TDecompressionStream.Create(AStream);
      try
        try
          LTempStream.CopyFrom(LCompressedStream, LCompressedStream.size);
        finally
          LCompressedStream.Free;
        end;
      finally
        AStream.Clear;
        AStream.CopyFrom(LTempStream, 0);
        AStream.Seek(0, soBeginning);
        Result := True;
      end;
    finally
      LTempStream.free;
    end;
  except
    Result := False;
  end;
end;

The cipher is initialized using the InitCipherWithKey() method. It is designed to convert a MD5 hash into it's binary representation, contained in the LMD5Hash variable (yes, the array is 64 bytes long but only the first 16 will be used by the cipher, since I call Init() with the 128 value (which means a 128-bit/16 bytes key length) :

procedure InitCipherWithKey(ACipher: TDCP_cipher; const AKey: AnsiString);
var
  LMD5Hash: array [0..63] of Byte;
  S: AnsiString;
begin
  //We use a 128 bit key
  ZeroMemory(@LMD5Hash, SizeOf(LMD5Hash));
  S := AKey;
  HexToBin(PAnsiChar(S), LMD5Hash, Length(LMD5Hash) -1);
  ACipher.Init(LMD5Hash[0], 128, nil);
  ZeroMemory(@LMD5Hash, SizeOf(LMD5Hash));
end;

Thanks in advance.

Upvotes: 1

Views: 1783

Answers (1)

Maarten Bodewes
Maarten Bodewes

Reputation: 93968

There is something going wrong when calculating/transporting the key. First you calculate the key from a password (incorrectly named AKey) using MD5. For some reason, you are using a buffer of 64 bytes for this, even though MD5 will always output exactly 16 bytes. This is already not secure, you should use a password based key derivation function such as PBKDF2 to derive keys from passwords.

Then however you seem to treat the binary output of MD5 as characters. I presume you take the output of InitCipherWithKey and put it in AParameters. Now suddenly the binary output of MD5 is treated as a string. Unfortunately, this means that bytes are interpreted as characters such as control characters (including the null-termination character value if the byte has value 00).

So it is most likely that the decryption does not succeed because of loss of data from the key itself. This of course depends on the key value, making this scheme fail now and then. Please check your code base to see if you make errors regarding and .

Upvotes: 1

Related Questions