Using Hexlicense server-side

HexLicense is a component based architcture. It is written in standard object pascal with no dependencies on system libraries, external DLL’s or operative system functionality.

This means that HexLicense can be used both by VCL visual applications, non-visual applications; Firemonkey desktop applications, mobile applications and services.

Using HexLicense to build a serial server

Many customers have asked if HexLicense can be implemented server-side, effectively moving serial number identification and validation away from local applications.

This is ofcourse possible. In fact, one of the reasons HexLicense was organized as non-visual components was to ensure that it could be deployed in different situations. Not just in single executables.

Being able to use the HexLicense components on your server does not mean that no validation need occur in the program. Actually, your program should implement HexLicense as usual.

It must be understood that the server-side validation is for your benefit, not the customer.

Keeping track of licenses

A classical scheme for keeping track of licenses is (just an example):

  • A database with all generated serial numbers, customers and root-keys
  • A RPC or REST service exposing validation methods
  • Capability to mark a serial number as “bought” and also store a disk checksum or serial number from the customer.
  • Small list of roughly 10 records keeping track of when your program was last used (time, date), network mac address and IP address should also be stored in this table

The startup sequence

When your application starts, HexLicense will issue its events and you should validate the license as per usual; including expiration (please see introduction and quick-start on how to use Hexlicense) of the license or otherwise factors in need of attention.

When your license and session is considered valid, you should request a harddisk checksum or serial number, which identifies the harddisk your software has been installed to.

When calling your RPC/REST service, the following information should be supplied:

  • Application serial number
  • Harddisk serial number or checksum
  • Duration of the license (expiration date, bought date)
  • Remote IP
  • User’s network MAC address

When invoked, your server should look-up the serial number in the database. A bought license should already be registered to a customer and the license marked as “valid”.

If the harddisk serial number is registered (which you should send to your server when your product is first bought) it should be compared with the one sent as a parameter.

Also, the bought-date and expiration-date on file should be compared to the information given as parameters in the validation call.

If all matches up, the server should return a OK and the program continue.

If it doesnt match up, you have to chose how to respond. You can:

  • Log IP address of license offender for use in legal persecution
  • Mark license as invalid, effectively terminating relation with that customer
  • Mark license as invalid server-side, clear license locally and reduce the application back to trial mode.

I would urge you to respond with the latter two options: terminate your relationship with the customer based on breach of license agreement, and further clear the license locally and reduce the application back to trial mode.

It depends greatly on how much time and resources you are willing to spend hunting down pirates in other contries (and possibly continents). In some cases it may be worth while, but history demonstrates that legal action against a customer in another contry is difficult.

Multiple use of the same license

You should log the use of licenses regardless of the above, especially the time, mac and IP address is of interest here.

If a license is used at the same time, but in different locations; something you can pick up by tracking the IP through a whois() service, which can be automated via Delphi’s Indy components, it is highly likely that your software license is being used on more than one computer.

The use of WhoIs is quite vital here, because if your software has been installed on a virtual-machine (VMWare, VirtualBox, Bochs etc), all the hardware information will be identical. The only thing that uniquely identifies each user is the IP, which can be traced back to an ISP or national region. Customers rarely change ISP and region, so this gives you an indication.

NOTE: ISP/Region checks should only be performed when your server notice that the same license is being used by different IP’s at the same time. Customer can after all move house or use your program on a laptop traveling (hence they will have a different ISP depending on where they are).

If the use of a license is noticed during the same time-frame, or from an IP not in the same locale (look up the IP using whois() services) it is highly possible that someone is using the same license on different machines. Its important to use the WhoIs() service here, otherwise it may be difficult to distinguish between virtual computers should the software be installed on a virtual machine and copied.

Getting information

There are plenty of examples on how to obtain a harddisk serial number, the network MAC and IP address online. Use google to find a solution which best matches your program.

You will also find plenty of solutions on websites like Torry’s Delphi pages, Github and Google code (depricated).

Below are examples on how you can obtain system information. Please note that these are only examples from various places around the internet. They must always be tested, and you should preferably look up the various API calls to make sure they are not deprecated before using this code.

We do not provide any warranty regarding the code below. They are provided as examples only, and you use the code purely at your own risk.

Getting the bios identifier and harddisk serial number for the boot-drive can be done as such:

uses
  ActiveX,
  ComObj;

  var
    FSWbemLocator : OLEVariant;
    FWMIService   : OLEVariant;

function  GetWMIstring(const WMIClass, WMIProperty:string): string;
const
  wbemFlagForwardOnly = $00000020;
var
  FWbemObjectSet: OLEVariant;
  FWbemObject   : OLEVariant;
  oEnum         : IEnumvariant;
  iValue        : LongWord;
begin;
  Result:='';
  FWbemObjectSet:= FWMIService.ExecQuery(Format('Select %s from %s',[WMIProperty, WMIClass]),'WQL',wbemFlagForwardOnly);
  oEnum         := IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
  if oEnum.Next(1, FWbemObject, iValue) = 0 then
  if not VarIsNull(FWbemObject.Properties_.Item(WMIProperty).Value) then
  Result:=FWbemObject.Properties_.Item(WMIProperty).Value;
  FWbemObject:=Unassigned;
end;

function GetHarddiskSerial(var Bios,Media:String):Boolean;
begin
  result := false;

  try
    FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
    FWMIService   := FSWbemLocator.ConnectServer('localhost', 'root\CIMV2', '', '');
    Bios:=GetWMIstring('Win32_BIOS','SerialNumber');
    Media:=GetWMIstring('Win32_PhysicalMedia','SerialNumber');
  except
    on exception do
    begin
      Bios := '';
      Media := '';
      exit;
    end;
  end;

  result := true;
end;

Getting the local IP address can be done in multiple ways, here is one which obtains it directly from winsocket:


uses Winsock;

Function GetIPAddress():String;
type
  pu_long = ^u_long;
var
  varTWSAData : TWSAData;
  varPHostEnt : PHostEnt;
  varTInAddr : TInAddr;
  namebuf : Array[0..255] of char;
begin
  If WSAStartup($101,varTWSAData) <> 0 Then
  Result := 'No. IP Address'
  Else Begin
    gethostname(namebuf,sizeof(namebuf));
    varPHostEnt := gethostbyname(namebuf);
    varTInAddr.S_addr := u_long(pu_long(varPHostEnt^.h_addr_list^)^);
    Result := 'IP Address: '+inet_ntoa(varTInAddr);
  End;
  WSACleanup;
end;

Obtaining the active network adapter’s MAC address can likewise be done in a variety of ways, here is one example calling netbios directly for the information:

uses nb30;

function GetMACAdress: string;
var
  NCB: PNCB;
  Adapter: PAdapterStatus;

  URetCode: PAnsiChar;
  RetCode: Ansichar;
  I: integer;
  Lenum: PlanaEnum;
  _SystemID: string;
  TMPSTR: string;
begin
  Result    := '';
  _SystemID := '';
  Getmem(NCB, SizeOf(TNCB));
  Fillchar(NCB^, SizeOf(TNCB), 0);

  Getmem(Lenum, SizeOf(TLanaEnum));
  Fillchar(Lenum^, SizeOf(TLanaEnum), 0);

  Getmem(Adapter, SizeOf(TAdapterStatus));
  Fillchar(Adapter^, SizeOf(TAdapterStatus), 0);

  Lenum.Length    := chr(0);
  NCB.ncb_command := chr(NCBENUM);
  NCB.ncb_buffer  := Pointer(Lenum);
  NCB.ncb_length  := SizeOf(Lenum);
  RetCode         := Netbios(NCB);

  i := 0;
  repeat
    Fillchar(NCB^, SizeOf(TNCB), 0);
    Ncb.ncb_command  := chr(NCBRESET);
    Ncb.ncb_lana_num := lenum.lana[I];
    RetCode          := Netbios(Ncb);

    Fillchar(NCB^, SizeOf(TNCB), 0);
    Ncb.ncb_command  := chr(NCBASTAT);
    Ncb.ncb_lana_num := lenum.lana[I];
    // Must be 16
    Ncb.ncb_callname := '*               ';

    Ncb.ncb_buffer := Pointer(Adapter);

    Ncb.ncb_length := SizeOf(TAdapterStatus);
    RetCode        := Netbios(Ncb);
    //---- calc _systemId from mac-address[2-5] XOR mac-address[1]...
    if (RetCode = chr(0)) or (RetCode = chr(6)) then
    begin
      _SystemId := IntToHex(Ord(Adapter.adapter_address[0]), 2) + '-' +
        IntToHex(Ord(Adapter.adapter_address[1]), 2) + '-' +
        IntToHex(Ord(Adapter.adapter_address[2]), 2) + '-' +
        IntToHex(Ord(Adapter.adapter_address[3]), 2) + '-' +
        IntToHex(Ord(Adapter.adapter_address[4]), 2) + '-' +
        IntToHex(Ord(Adapter.adapter_address[5]), 2);
    end;
    Inc(i);
  until (I >= Ord(Lenum.Length)) or (_SystemID <> '00-00-00-00-00-00');
  FreeMem(NCB);
  FreeMem(Adapter);
  FreeMem(Lenum);
  GetMacAdress := _SystemID;
end;