Bob Swart (aka Dr.Bob)
Versleutelde ClientDataSets

In dit artikel combineer ik code en bevindingen over het versleutelen van ClientDataSets die eerder zijn verschenen in twee van mijn Under Construction artikelen in The Delphi Magazine nummer 78 en 86, en voeg meteen enkele nieuwe benaderingen toe. Ik zal laten zien hoe we de inhoud van een ClientDataSet kunnen opslaan in een versleuteld bestand op schijf, en hoe we met behulp van de Socket Server en TSocketConnection component ook de inhoud van een data packet kunnen versleutelen als het wordt verstuurd van een DataSnap server naar de clients of andersom.

Versleutelde Filestreams
Om te beginnen met het opslaan van de inhoud van een ClientDataSet als een versleuteld bestand: daar hebben we eigenlijk alleen maar een speciale stream voor nodig die de informatie kan versleutelen. Daarnaast is het ook mogelijk om een component te gebruiken dat streams kan versleutelen (zoals te vinden in TurboPower's LockBox library bijvoorbeeld - zie http://www.turbopower.com/products/lockbox/components - inmiddels ook te vinden op SourceForge door het einde van TurboPower Software).
Om te laten zien hoe de techniek zelf werkt - los van de kracht van de versleuteling - zal ik hier een heel eenvoudige encryptie gebruiken, gebasseerd op de XOR operator. Ieder teken uit de stream zal ik met XOR $42 vervangen door een ander teken. Het voordeel van de XOR voor dit voorbeeld is dat deze zijn eigen inverse is (dus twee keer versleutelen en je hebt het origineel weer terug). Daarnaast is dit een versleutelingstechniek waarbij je geen sleutels nodig hebt. Niet echt krachtig, maar zoals gezegd zijn er verschillende sterkere technieken te bouwen of te koop - ik wil hier met name laten zien hoe je een-en-ander kan inpassen.

  unit B42EncryptedFileStream;
  interface
  uses
    Classes;

  type
    TB42EncryptedFileStream = class(TFileStream)
    public
      function Read(var Buffer; Count: Integer): Integer; override;
      function Write(const Buffer; Count: Integer): Integer; override;
    end;

  implementation

  { TB42EncryptedFileStream }

  function TB42EncryptedFileStream.Read(var Buffer; Count: Integer): Integer;
  var
    i: Integer;
    Buf: String;
  begin
    Result := inherited Read(Buffer, Count);
    SetLength(Buf, Count);
    Move(Buffer, PChar(@Buf[1])^, Count);
    for i:=1 to Count do
      Buf[i] := Chr(Ord(Buf[i]) XOR $42); // decode
    Move(PChar(@Buf[1])^, Buffer, Count)
  end;

  function TB42EncryptedFileStream.Write(const Buffer; Count: Integer): Integer;
  var
    i: Integer;
    Buf: String;
  begin
    SetLength(Buf, Count);
    Move(Buffer, PChar(@Buf[1])^, Count);
    for i:=1 to Count do
      Buf[i] := Chr(Ord(Buf[i]) XOR $42); // encode
    Result := inherited Write(PChar(@Buf[1])^, Count)
  end;

  end.

Als we eenmaal een versleutelstream gemaakt (of gekocht) hebben, is het zaak om een eigen TB42EncryptedClientDataSet component af te leiden van de normale TClientDataSet, en daarbij de twee nieuwe methodes SaveToEncryptedFile en LoadFromEncryptedFile toe te voegen.
Start Delphi, doe File | New - Other, selecteer de Component icon en klik op OK. In de dialoog moeten we aangeven TClientDataSet als voorvader te gebruiken, en de nieuwe component de naam TB42EncryptedClientDataSet te willen noemen:

Voeg vervolgens twee nieuwe procedures toe, genaamd SaveToEncryptedFile en LoadFromEncryptedFile. Beiden krijgen een FileName als parameter mee, en ook al een optionele parameter genaamd Key. Die zullen we in dit artikel niet nodig hebben, maar kan gebruikt worden bij de implementatie van een sterkere versleuteling (en dan hoef je de definitie van de twee procedures later niet nog een keer aan te passen).

  unit B42EncryptedClientDataSet;
  interface
  uses
    SysUtils, Classes, DB, DBClient;

  type
    TB42EncryptedClientDataSet = class(TClientDataSet)
    public
      procedure SaveToEncryptedFile(const FileName: String; const Key: String = '');
      procedure LoadFromEncryptedFile(const FileName: String; const Key: String = '');
    end;

  procedure Register;

  implementation
  uses
    B42EncryptedFileStream; // zie vorige Listing

  procedure Register;
  begin
    RegisterComponents('eBob42', [TB42EncryptedClientDataSet]);
  end;

  { TB42EncryptedClientDataSet }

  procedure TB42EncryptedClientDataSet.LoadFromEncryptedFile(const FileName, Key: String);
  var
    FileStream: TB42EncryptedFileStream;
  begin
    FileStream := TB42EncryptedFileStream.Create(FileName, fmCreate);
    try
      SaveToStream(FileStream, dfBinary)
    finally
      FileStream.Free
    end
  end;

  procedure TB42EncryptedClientDataSet.SaveToEncryptedFile(const FileName, Key: String);
  var
    FileStream: TB42EncryptedFileStream;
  begin
    if Active then EmptyDataSet; // clear contents
    FileStream := TB42EncryptedFileStream.Create(FileName, fmOpenRead);
    try
      LoadFromStream(FileStream)
    finally
      FileStream.Free
    end
  end;

  end.

Het gebruik van deze nieuwe TB42EncryptedClientDataSet component is net als de originele TClientDataSet, behalve dan dat we nu methoden hebben om de inhoud versleuteld op schijf op te slaan.
Wie serieus gebruik wil maken van versleutelde (ClientDataSet) bestanden zou zeker de Lock Box library van TurboPower eens nader moeten bekijken om tot een (veel) sterkere versleuteling te komen - voor relatief weinig geld.

DataSnap
Het versleutelen van lokale ClientDataSets heeft met name zin heeft bij het beschermen van data voor relatief kleine lokale toepassingen. Er bestaat echter ook nog iets als DataSnap, waarbij de inhoud van een ClientDataSet van de ene tier naar de andere wordt verzonden: een DataSnap server die met één of meerdere DataSnap clients praat. Hierbij is het theoretisch mogelijk dat een data packet onderweg wordt onderschept (door kwaadwillenden), en zou je ook hier dus kunnen overwegen om enige vormen van versleuteling te gaan gebruiken. Deze versleuteling zal dan wel zowel bij de DataSnap server als bij alle verbonden DataSnap clients geïmplementeerd moeten zijn.
Als we gebruik maken van TCP/IP Sockets als communicatie protocol, dan verloopt de daadwerkelijke communicatie via de zgn. Borland Socket Server (scktsrvr.exe uit de Delphi7\bin directory). Dit is een kleine toepassing die we op de machine moeten draaien waar de DataSnap Server staat (de Borland Socket Server praat dan via COM met de DataSnap Server, en alle DataSnap clients bevatten een TSocketConnection component die via TCP/IP sockets met de Borland Socket Server en daarmee (indirekt) met de DataSnap Server zelf praat).
In een dergelijke opstelling kunnen we een "interceptor" installeren die bij de data packets kan die tussen de Borland Socket Server (aan de server kant) en de TSocketConnection component (aan de client kant) heen-en-weer worden gezonden. Een dergelijke interceptor kan vanaf Delphi 6 op eenvoudige wijze gebouwd worden door het IDataIntercept interface te implementeren. Dit bestaat uit de volgende methoden:

  IDataIntercept = interface
  ['{B249776B-E429-11D1-AAA4-00C04FA35CFA}']
    procedure DataIn(const Data: IDataBlock); stdcall;
    procedure DataOut(const Data: IDataBlock); stdcall;
  end;
waarbij het IDataBlock interface er op zijn beurt als volgt uit ziet:
  IDataBlock = interface(IUnknown)
  ['{CA6564C2-4683-11D1-88D4-00A0248E5091}']
    function GetBytesReserved: Integer; stdcall;
    function GetMemory: Pointer; stdcall;
    function GetSize: Integer; stdcall;
    procedure SetSize(Value: Integer); stdcall;
    function GetStream: TStream; stdcall;
    function GetSignature: Integer; stdcall;
    procedure SetSignature(Value: Integer); stdcall;
    procedure Clear; stdcall;
    function Write(const Buffer; Count: Integer): Integer; stdcall;
    function Read(var Buffer; Count: Integer): Integer; stdcall;
    procedure IgnoreStream; stdcall;
    function InitData(Data: Pointer; DataLen: Integer; CheckLen: Boolean): Integer; stdcall;
    property BytesReserved: Integer read GetBytesReserved;
    property Memory: Pointer read GetMemory;
    property Signature: Integer read GetSignature write SetSignature;
    property Size: Integer read GetSize write SetSize;
    property Stream: TStream read GetStream;
  end;

De Stream zelf bestaat uit een Signature gevolgd door de daadwerkelijke data zelf. Het is belangrijk dat we de header zelf niet proberen te versleutelen (dat leidde in mijn geval tot een niet-werkend geheel), maar wel de data die erna volgt. Deze keer is er geen sprake van een elegante splitsing van de versleuteling, maar is het - wederom eenvoudige - XOR $42 voorbeeld direkt in de implementatie van de DataIn en DataOut methoden zelf terug te vinden in onderstaande code:

  library Intercept42;
  uses
    ComServ, ComObj, SConnect, SysUtils, Classes;

  type
    TSpy42 = class(TComObject, IDataIntercept)
    public
      procedure DataIn(const Data: IDataBlock); stdcall;
      procedure DataOut(const Data: IDataBlock); stdcall;
    end;

  const
    Spy42_GUID: TGUID = '{DCED4111-4268-4726-87C0-C4FC45592286}';

  exports
    DllGetClassObject,
    DllCanUnloadNow,
    DllRegisterServer,
    DllUnregisterServer;

  { TSpy42 }

  const
    SizeOfHeader = 8;

  procedure TSpy42.DataIn(const Data: IDataBlock);
  { report, decrypt or decompress }
  var
    i: Integer;
  var
    InStream,OutStream: TMemoryStream;
    Size: Integer;
  begin
    // Decrypting
    if Data.Size > SizeOfHeader then
    begin
      InStream := TMemoryStream.Create;
      try
        { Skip BytesReserved bytes of data }
        InStream.Write(Pointer(Integer(Data.Memory) + Data.BytesReserved)^, Data.Size);
        Size := InStream.Size;
        if Size = 0 then Exit;
        OutStream := TMemoryStream.Create;
        try
          OutStream.CopyFrom(InStream, 0);
          for i:=SizeOfHeader to Pred(OutStream.Size) do
            PChar(OutStream.Memory)[i] :=
              Char(Ord(PChar(OutStream.Memory)[i]) XOR $42);
        { Clear the datablock, write the encrypte data into the datablock }
          Data.Clear;
          Data.Write(OutStream.Memory^, OutStream.Size)
        finally
          OutStream.Free
        end
      finally
        InStream.Free
      end
    end
  end;

  procedure TSpy42.DataOut(const Data: IDataBlock);
  { report, encrypt or compress }
  var
    i: Integer;
  var
    InStream,OutStream: TMemoryStream;
    Size: Integer;
  begin
    // Encrypting
    if Data.Size > SizeOfHeader then
    begin
      InStream := TMemoryStream.Create;
      try
        { Skip BytesReserved bytes of data }
        InStream.Write(Pointer(Integer(Data.Memory) + Data.BytesReserved)^, Data.Size);
        Size := InStream.Size;
        if Size = 0 then Exit;
        OutStream := TMemoryStream.Create;
        try
          OutStream.CopyFrom(InStream, 0);
          for i:=SizeOfHeader to Pred(OutStream.Size) do
          begin
            PChar(OutStream.Memory)[i] :=
              Char(Ord(PChar(OutStream.Memory)[i]) XOR $42)
          end;
        { Clear the datablock, write the encrypte data into the datablock }
          Data.Clear;
          Data.Write(OutStream.Memory^, OutStream.Size)
        finally
          OutStream.Free
        end
      finally
        InStream.Free
      end
    end
  end;

  begin
  { Use this class factory to allow for easy identification of Interceptors }
    TPacketInterceptFactory.Create(ComServer, TSpy42, Spy42_GUID,
      'Spy42', 'Interceptor', ciMultiInstance, tmApartment);
  end.

Merk op dat de Interceptor DLL een ActiveX Library is, en dus als zodanig ook geregistreerd dient te worden voor je hem goed kan gebruiken. Een ander voorbeeld van een interceptor is terug te vinden in de Delphi7\Demos\Midas\Intrcpt directory, waar in plaats van versleuteling voor compressie is gekozen (dit is met name zinvol in situaties waar sprake is of kan zijn van een bandbreedte bottle-neck).

Interceptor Installatie
Het installeren van de Interceptor DLL gaat eenvoudig, maar moet wel zorgvuldig aan beide kanten gebeuren: zowel in de Borland Socket Server als in de TSocketConnection component. Om met de eerste te beginnen: de Borland Socket Server heeft een speciale editbox waar je de GUID van de Interceptor DLL kan invullen (copy-en-paste lijkt hier de meest voor de hand liggende manier).

En nu we het toch over de Borland Socket Server hebben in combinatie met veiligheid: kijk nog eens naar de screenshot hierboven. Let vooral op de waarde van Port die op de default waarde 211 staat. Dat is de waarde die default door zowel de TSocketConnection component als de Borland Socket Server wordt aangenomen, en de poort die ik dus default zou afluisteren als ik een hacker was op zoek naar "beschikbare" DataSnap servers. Het is een kleine moeite om dit poort nummer aan te passen van 211 naar iets anders - het maakt niet zoveel uit welk nummer (zolang het niet al in gebruik is). Let op dat je dit zowel in de Borland Socket Server als in de TSocketConnection component moet doen (anders kunnen de DataSnap clients de Socket Server niet meer vinden, en valt er weinig te beleven).
En behalve het nieuwe poortnummer, moeten we uiteraard ook het gebruik van de Interceptor aangeven in de TSocketConnection component. Hiervoor kun je de InterceptGUID property gebruiken, alhoewel het waarschijnlijk makkelijker is om de drop-down combobox van de InterceptName property te gebruiken (daarvoor moet je dan wel eerst de Interceptor DLL als ActiveX Library geregistreerd te hebben op je ontwikkelmachine).

Let tenslotte op dat je bij deployment van de DataSnap clients ook de Interceptor DLL meelevert (net als de MIDAS.dll - tenzij je die meelinkt met de executable als unit MidasLib).

Wie gebruik wil maken van een dergelijke compressie of versleuteling, maar daarbij geen gebruik wil maken van TCP/IP Sockets als communicatie protocol (maar van DCOM, HTTP of SOAP) kan geen gebruik maken van bovenstaande techniek. In dat geval zul je weer terug moeten naar de ClientDataSet component (alsmede de DataSetProvider component). Het is in dat geval de XMLData property van type OleData waar de data in XML formaat staat, waar de aandacht op gericht moet worden. Ik heb hier zelf nog geen ervaring mee, maar heb van verschillende kanten gehoord dat dit de aangewezen plek is om eigen compressie en/of versleutelingen toe te passen. Wellicht onderwerp voor een vervolg op dit verhaal.

Meer Informatie
Mocht iemand nog vragen, opmerkingen of suggesties hebben, dan hoor ik die het liefst via . Wie meer wil zien betreffende Delphi en versleutelden ClientDataSets, zou zeker eens een bezoek aan mijn Delphi en DataSnap Clinic moeten overwegen.


Dit artikel is eerder verschenen in SDGN Magazine #75 - december 2002

This webpage © 1999-2006 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.