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.
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.
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... if (RetCode = chr(0)) or (RetCode = chr(6)) then begin _SystemId := IntToHex(Ord(Adapter.adapter_address), 2) + '-' + IntToHex(Ord(Adapter.adapter_address), 2) + '-' + IntToHex(Ord(Adapter.adapter_address), 2) + '-' + IntToHex(Ord(Adapter.adapter_address), 2) + '-' + IntToHex(Ord(Adapter.adapter_address), 2) + '-' + IntToHex(Ord(Adapter.adapter_address), 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;