In my book “Visual programming” I described a simple licensing system based on hardware coding algorithms attributes of the computer on which the software is being installed. The algorithm is simple and reliable, but, as practice has shown, modern marketing trends require a more flexible approach to software licensing: in addition to a lifetime license, two more categories of licenses are required:

  • Licenses for a specific version of the program
  • Licenses for individual modules of the program

To do this, I developed a set of scripts for My Visual Database, with the help of which licensing is implemented, including versioned and modular. Thanks to a well-thought-out algorithm, scripts can be included in any of your projects: when the subsystem is initialized, it automatically creates all the necessary user interface elements, including the registration form.

Algorithm

The algorithm for validating a license key is based on comparing it with a calculated value, which is obtained by calculating the MD5 hash for a string expression.

License typeComposition of an expressionComment
LifetimeHardware key + secret word
VersionedHardware key + secret word + version numberThe major version number is used, the license is valid for all sub-versions of the software
ModularHardware key + secret word + module prefix + module listThe module list is a comma-separated list of module numbers
Versioned modularHardware Key + Secret Word + Version Number + Module Prefix + Module List

To determine the hardware key, the App_GetHWKey() function is used, which combines the serial number of the C: partition and the MAC address.

function App_GetHWKey: string;
// returns unique hardware key
begin
  try
    Result := StrToMD5 ( GetFirstMacAddress + GetHardDiskSerial('c:') );
  except // in rare cases, disk c: may not be
    RaiseException('App_GetHWKey() - C: drive not found');
  end;
end;
Code language: Delphi (delphi)

The C: partition serial number alone may not be enough, as it is easily edited by some disk maintenance tools. At the same time, there were cases when the user changed the network settings (enabled/disabled the wifi adapter) and at the same time changed the MAC address returned by the GetFirstMacAddress() function, which caused the licensing system to fail. The same failure can be caused by anonymizers that change the poppy address. Therefore, when implementing this function, you will need to choose the least evil. Or come up with a more reliable algorithm.

I placed this function in the App module as I plan to use it for network authentication and it may come in handy in other modules of the program. In the same place, I added functions that are responsible for displaying the version number. The string value version has been converted to two numbers: the major and minor parts of the number, separated by a dot when displayed. The App_GetVersion() function is now responsible for the appearance of the version.

To determine the hardware key, the App_GetHWKey() function is used, which combines the serial number of the C: partition and the MAC address.

function App_GetHWKey: string;
// returns unique hardware key
begin
  try
    Result := StrToMD5 ( GetFirstMacAddress + GetHardDiskSerial('c:') );
  except // in rare cases, disk c: may not be
    RaiseException('App_GetHWKey() - C: drive not found');
  end;
end;
Code language: Delphi (delphi)

The C: partition serial number alone may not be enough, as it is easily edited by some disk maintenance tools. At the same time, there were cases when the user changed the network settings (enabled/disabled the wifi adapter) and at the same time changed the MAC address returned by the GetFirstMacAddress() function, which caused the licensing system to fail. The same failure can be caused by anonymizers that change the poppy address. Therefore, when implementing this function, you will need to choose the least evil. Or come up with a more reliable algorithm.

I placed this function in the App module as I plan to use it for network authentication and it may come in handy in other modules of the program. In the same place, I added functions that are responsible for displaying the version number. The string value version has been converted to two numbers: the major and minor parts of the number, separated by a dot when displayed. The App_GetVersion() function is now responsible for the appearance of the version.

procedure License_Check;
// check the license and, if necessary, display various inscriptions
var
  tmpKey:string; // cache data
  tmpHWKey:string;
begin
  tmpKey := IniFile_Read(LICENSE_SECT,LICENSE_KEY,''); // cache data
  if tmpKey = '' then
    Licensed := False
  else
  begin
    tmpHWKey := App_GetHWKey;
    // set a global variable that can be used to restrict functionality
    //
    // full lifetime license
    Licensed := tmpKey = StrToMD5 ( tmpHWKey + LICENSE_SECRET_WORD );
    // version license
    if not licensed then
      Licensed := tmpKey = StrToMD5 ( tmpHWKey + LICENSE_SECRET_WORD + IntToStr(APP_VERSION) );
    // lifetime modular license
    if not licensed then
      Licensed := License_CheckModules( tmpKey, tmpHWKey + LICENSE_SECRET_WORD );
    // modular version license
    if not licensed then
      Licensed := License_CheckModules( tmpKey, tmpHWKey + LICENSE_SECRET_WORD + IntToStr(APP_VERSION) );
  end;
  // if necessary, change the inscriptions
  License_UpdateAboutForm; // on the "About" form
  App_UpdateMainForm; // on the main form
end;
Code language: PHP (php)

To check a modular license, an additional function License_CheckModules() was required, which iterates over all possible combinations of modules to determine which one corresponds to a given license.

function License_CheckModules(AKey:string; AData:string):boolean;
// selection of combinations (combinations) from module numbers.
var
  i: integer;
  k: integer; // how many bits
  p: integer; // digit to increase
  A: array of integers; // storing a combination of modules as numbers
  s:string // assembly of numbers as a string
begin
  License_Modules := '';
  for k := 1 to LICENSE_MODULE_COUNT do
  begin
    SetLength(A, k+1 ); // null element is not used
    for i := 1 to k do
      A[i] := i; // first subset
    p := k; // let's increase the rightmost digit
    while p >= 1 do // end if digits run out
    begin
      // form the assembly as a string
      s:='';
      for i:=1 to k do
        s := s + IntToStr(A[i])+',';
      delete(s,length(s),1);
      // check
      Result := AKey = StrToMD5 ( AData + LICENSE_MOD + s );
      if result then
      begin
        License_Modules := s;
        exit;
      end;
      // exit from the loop if we checked the assembly, consisting of all elements
      if k = LICENSE_MODULE_COUNT then
        break;
      // shift the pointer if the last element reached the maximum
      if A[k] = LICENSE_MODULE_COUNT then
      begin
        p := p - 1;
      end
      else
        p := k;
      // if the pointer is not out of bounds, then generate the next combination
      if p >= 1 then
        for i := k downto p do
        begin
          A[i] := A[p] + i - p + 1;
        end;
    end;
  end;
end;Code language: Delphi (delphi)

Thus, the system turned out to be universal: with one key, you can allow the program to work with any combination of modules, with or without version. The result of the check is stored in two variables: Licensed and License_Modules. The License_ModuleNames array is intended to store human-readable names for modules, the list of which is formed using the License_GetModulesNames() function.

var
  Licensed: boolean // check if the program has a license
  License_Modules: string; // module licenses: numbers separated by commas
  License_ModuleNames: array of string; // module licenses: names

function License_GetModulesNames():string;
// Get a list of licensed modules
var
  tmpList: array of string;
  i:integer;
begin
  Result := '';
  tmpList := SplitString(License_Modules,',');
  for i:=0 to Length(tmpList) - 1 do
  begin
    Result := Result + License_ModuleNames[ StrToInt( tmpList[i] ) ] + ', ';
  end;
  if Result <> ''then
    Delete(Result,Length(Result)-1,2);
end;Code language: Delphi (delphi)

And for everything to work, it is enough to call the initialization procedure. Below is an initialization example for five modules. Module numbers start with one.

procedure License_Init;
// initialization of the licensing subsystem
begin
   License_CreateRegForm;
   // modules start at one.
   SetLength(License_ModuleNames,LICENSE_MODULE_COUNT+1);
   // Module names displayed in the "About" window
   License_ModuleNames[1] := 'First module';
   License_ModuleNames[2] := 'Second module';
   License_ModuleNames[3] := 'Third module';
   License_ModuleNames[4] := 'Fourth module';
   License_ModuleNames[5] := 'Fifth module';
   // license check
   License_Check;
end;
Code language: Delphi (delphi)

App integration

In addition to the App_GetVersion function, there are a few more procedures that are used to interact with the licensing subsystem. In particular, using the License_UpdateAboutForm() procedure, information about the licensor (1), license key (2), and active modules (3) is added to the standard “About” form. As well as a button to display the registration form (4).

procedure License_UpdateAboutForm;
// update license information on the "About" form
var
  tmpLabel: TLabel;
  tmpLabLink: TLabel;
  tmpButton: TButton;
  tmpButOK: TButton;
begin
  // version number
  FindC(frmdbCoreAbout,'labVersion',tmpLabel,False);
  if tmpLabel<>nil then
    tmpLabel.Caption := 'Version '+App_GetVersion;
  // licensor and license key
  FindC(frmdbCoreAbout,'LinkLabel1',tmpLabLink); // for binding
  FindC(frmdbCoreAbout,'Button1',tmpButOK); // for binding
  //
  FindC(frmdbCoreAbout,'labLicensee',tmpLabel,False);
  if tmpLabel=nil then
  begin
    tmpLabel := TLabel.Create(frmdbCoreAbout);
    with tmpLabel do
    begin
      Name := 'labLicense';
      Parent := tmpLabLink.Parent;
      Left := tmpLabLink.Left;
      Top := tmpLabLink.Top + 24 + 80 + 40;
      Font.Size := 11;
    end;
  end;
  tmpLabel.Caption := 'Licensee: '+IniFile_Read(LICENSE_SECT,LICENSE_LICENSE,'');
  //
  FindC(frmdbCoreAbout,'labLicenseKey',tmpLabel,False);
  if tmpLabel=nil then
  begin
    tmpLabel := TLabel.Create(frmdbCoreAbout);
    with tmpLabel do
    begin
      Name := 'labLicenseKey';
      Parent := tmpLabLink.Parent;
      Left := tmpLabLink.Left;
      Top := tmpLabLink.Top + 24*2 + 80+40;
      Font.Size := 11;
    end;
  end;
  tmpLabel.Caption := 'Key: '+IniFile_Read(LICENSE_SECT,LICENSE_KEY,'');
  //
  FindC(frmdbCoreAbout,'labModuleList',tmpLabel,False);
  if tmpLabel=nil then
  begin
    tmpLabel := TLabel.Create(frmdbCoreAbout);
    with tmpLabel do
    begin
      Name := 'labModuleList';
      Parent := tmpLabLink.Parent;
      Left := tmpLabLink.Left;
      Top := tmpLabLink.Top + 24*3 + 80+40;
      AutoSize := False;
      Width := 400;
      Height := 72;
      WordWrap := True;
      Font.Size := 11;
    end;
  end;
  tmpLabel.Caption := License_GetModulesNames();
  //
  FindC(frmdbCoreAbout,'btnRegister',tmpButton,False);
  if tmpButton = nil then
  begin
    tmpButton := TButton.Create(frmdbCoreAbout);
    with tmpButton do
    begin
      Name := 'btnRegister';
      Parent := tmpButOK.Parent;
      Left := tmpButOK.Left - 134;
      Width := 130;
      Top := tmpButOK.Top;
      Font.Size := 11;
      Default := False;
      Caption := 'Register'+YA;
      OnClick := @License_About_btnRegister_OnClick;
    end;
  end;
// tmpButton.Visible := not Licensed;
end;
Code language: Delphi (delphi)

Since the registration form is created programmatically, it can only be called using the App_GetFormByName() function, which returns the form by name:

procedure License_About_btnRegister_OnClick( Sender: TObject; var Cancel:boolean );
// handle pressing the registration button on the "About" form
begin
  App_GetFormByName('frmAppReg').ShowModal;
end;
Code language: JavaScript (javascript)

Registration form

Creating a form programmatically is a painstaking and time-consuming process compared to using the visual form designer. Is it justified? Let’s compare both approaches first.

ProcessesVisual constructorProgram code
CreationFast, immediate resultsSlow, application needs to be launched to view results
EditingFastSlow
Transfer to other projectsRequires manual copying of form elements.Fast, just copy the file with scripts and include it in the project

As you can see, if you do not need to transfer functionality to other projects, then it is easier to use the visual designer. But in the case of the licensing subsystem, I decided to first make a prototype using the visual constructor, and then implement its creation programmatically. And here’s what I came up with.

procedure License_CreateRegForm;
// creating a registration form
var
  tmpForm:TForm;
  tmpPC:TdbPageControl;
  tmpTab:TdbTabSheet;
  tmpLabel: TLabel; // Don't use TdbLabel - style issues
  tmpMemo: TdbMemo;
  tmpCheckBox: TdbCheckBox;
  tmpButton: TdbButton;
  tmpEdit: TdbEdit;
  tmpFileName:string;
begin
  tmpForm := TForm.Create(Application);
  with tmpForm do
  begin
    Name := 'frmAppReg';
    Caption := 'Registration ';
    Width := 387 + 16;
    Height := 308;
    BorderStyle := bsDialog;
    Font.Size := 11;
    Font Name := 'Segoe UI';
    OnShow := @License_RegForm_OnShow;
    Position := poScreenCenter;
  end;
  //
  tmpPC := TdbPageControl.Create(tmpForm);
  with tmpPC do
  begin
    Name := 'pgcMain';
    Parent := tmpForm;
    Align := alClient;
  end;
  /////////////////////////////////////////////////// ///////////////////////////////////////////
  //
  tmpTab := TdbTabSheet.Create(tmpForm);
  with tmpTab do
  begin
    Name := 'tshLicenseAgr';
    TabVisible := False;
    PageControl := tmpPC;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labLicenseAgr';
    Parent := tmpTab;
    Caption := 'License Agreement:';
    Left := 8;
    top := 8;
  end;
  //
  tmpMemo := TdbMemo.Create(tmpForm);
  with tmpMemo do
  begin
    Name := 'memLicenseAgr';
    Parent := tmpTab;
    Left := 8;
    top := 32;
    Width := 364+4;
    Height := 161;
    ScrollBars := ssVertical;
    Anchors := akTop+akLeft+akRight+akBottom;
    //
    tmpFileName := ExtractFilePath(Application.ExeName) + 'license.txt';
    if FileExists(tmpFileName) then
      Lines.LoadFromFile(tmpFileName)
    else
      RaiseException('License.txt file not found');
  end;
  //
  tmpCheckBox := TdbCheckBox.Create(tmpForm);
  with tmpCheckBox do
  begin
    Name := 'chbAccept';
    Parent := tmpTab;
    Left := 8;
    top := 200;
    Width := 290;
    Caption := 'Accept license agreement';
    OnClick := @License_RegForm_chbAccept_OnClick;
    Anchors := akLeft+akBottom;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnNext';
    Parent := tmpTab;
    Left := 238;
    top := 230;
    Width := 137;
    Height := 32;
    Caption := 'Next';
    OnClick := @License_RegForm_btnNext_OnClick;
    Anchors := akRight+akBottom;
  end;
  /////////////////////////////////////////////////// ///////////////////////////////////////////
  //
  tmpTab := TdbTabSheet.Create(tmpForm);
  with tmpTab do
  begin
    Name := 'tshLicenseData';
    TabVisible := False;
    PageControl := tmpPC;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labLicense';
    Parent := tmpTab;
    Caption := 'Licensor:';
    Left := 8;
    top := 8;
  end;
  //
  tmpEdit := TdbEdit.Create(tmpForm);
  with tmpEdit do
  begin
    Name := 'edtLicense';
    Parent := tmpTab;
    Left := 8;
    top := 32;
    Width := 364;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labRegistrationCode';
    Parent := tmpTab;
    Caption := 'Registration code:';
    Left := 8;
    top := 64;
  end;
  //
  tmpEdit := TdbEdit.Create(tmpForm);
  with tmpEdit do
  begin
    Name := 'edtRegistrationCode';
    Parent := tmpTab;
    Left := 8;
    top := 88;
    Width := 337;
    Height := 28;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnCopyToClipboard';
    caption := '';
    Parent := tmpTab;
    Left := 346;
    top := 88;
    Width := 28;
    Height := 28;
    Hint := 'Copy to clipboard';
    ShowHint := True;
    ImageAlignment := 2; // iaCenter
    OnClick := @License_RegForm_btnCopyToClipboard_OnClick;
  end;
  //
  tmpLabel := TLabel.Create(tmpForm);
  with tmpLabel do
  begin
    Name := 'labLicenseKey';
    Parent := tmpTab;
    Caption := 'License key:';
    Left := 8;
    top := 120;
  end;
  //
  tmpEdit := TdbEdit.Create(tmpForm);
  with tmpEdit do
  begin
    Name := 'edtLicenseKey';
    Parent := tmpTab;
    Left := 8;
    top := 144;
    Width := 364;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnBuy';
    Parent := tmpTab;
    Left := 8;
    Top := 230-6;
    Width := 130;
    Height := 32;
    Caption := 'Buy';
    OnClick := @License_RegForm_btnBuy_OnClick;
    Anchors := akLeft+akBottom;
  end;
  //
  tmpButton := TdbButton.Create(tmpForm);
  with tmpButton do
  begin
    Name := 'btnActivate';
    Parent := tmpTab;
    Left := 215;
    Top := 230-6;
    Width := 160;
    Height := 32;
    Caption := 'Activate';
    OnClick := @License_RegForm_btnActivate_OnClick;
    Anchors := akRight+akBottom;
  end;
  tmpPC.ActivePageIndex := 0;
end;

procedure License_RegForm_btnActivate_OnClick(Sender: TObject; var Cancel: boolean);
// Handling pressing the "Activate" button
var
  tmpForm: TForm;
  tmpLicenseKey: TdbEdit;
  tmpLicense: TdbEdit;
begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'edtLicenseKey',tmpLicenseKey);
  FindC(tmpForm,'edtLicensee',tmpLicensee);
  // write
  IniFile_Write(LICENSE_SECT,LICENSE_LICENSE,tmpLicensee.Text);
  IniFile_Write(LICENSE_SECT,LICENSE_KEY,tmpLicenseKey.Text);
  // check
  License_Check;
  // look
  if licensed then
  begin
    ShowMessage('License ownership confirmed');
    tmpForm.Close; // close the form
  end
  else
  begin
    // reset
    IniFile_Write(LICENSE_SECT,LICENSE_LICENSE,'');
    IniFile_Write(LICENSE_SECT,LICENSE_KEY,'');
    ShowMessage('A current license key is required');
  end;
end;

procedure License_RegForm_btnBuy_OnClick(Sender: TObject; var Cancel: boolean);
// Handling the "Buy" button click
begin
  OpenURL(LICENSE_BUY_LINK);
end;

// procedure License_RegForm_OnShow (Sender: TObject; Action: string);
procedure License_RegForm_OnShow(Sender: TObject;);
//
var
  tmpForm: TForm;
  tmpPC:TdbPageControl;
  tmpCheck: TdbCheckBox;
  tmpButton: TdbButton;
  tmpLicense: TdbEdit;
  tmpRegistrationCode: TdbEdit;
  tmpLicenseKey: TdbEdit;
begin
  tmpForm := TForm(Sender);
  FindC(tmpForm,'pgcMain',tmpPC);
  FindC(tmpForm,'chbAccept',tmpCheck);
  FindC(tmpForm,'btnNext',tmpButton);
  FindC(tmpForm,'edtLicensee',tmpLicensee);
  FindC(tmpForm,'edtRegistrationCode',tmpRegistrationCode);
  FindC(tmpForm,'edtLicenseKey',tmpLicenseKey);
  // open first tab
  tmpPC.ActivePageIndex := 0;
  tmpCheck.Checked := False;
  tmpButton.Enabled := False;
  // fill fields with data
  tmpLicensee.Text := IniFile_Read(LICENSE_SECT,LICENSE_LICENSE,'');
  tmpRegistrationCode.Text := App_GetHWKey;
  tmpLicenseKey.Text := IniFile_Read(LICENSE_SECT,LICENSE_KEY,'');
end;

procedure License_RegForm_btnNext_OnClick(Sender: TObject; var Cancel: boolean);
// Handling "Next" button click
var
  tmpForm: TForm;
  tmpPC:TdbPageControl;
begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'pgcMain',tmpPC);
  tmpPC.ActivePageIndex := 1;
end;

procedure License_RegForm_chbAccept_OnClick(Sender: TObject);
// unlock the button with a checker
var
  tmpForm:TForm;
  tmpButton:TdbButton;
  tmpChecker: TdbCheckbox;
begin
  tmpChecker := TdbCheckbox(Sender);
  CForm(Sender,tmpForm);
  FindC(tmpForm,'btnNext',tmpButton);
  tmpButton.Enabled := tmpChecker.Checked;
end;

procedure License_RegForm_btnCopyToClipboard_OnClick(Sender: TObject; var Cancel: boolean);
// copy registration code to clipboard
var
  tmpForm:TForm;
  tmpEdit:TdbEdit;
begin
  CForm(Sender,tmpForm);
  FindC(tmpForm,'edtRegistrationCode',tmpEdit);
  tmpEdit.SelectAll;
  tmpEdit.CopyToClipboard;
  tmpEdit.SelLength := 0;
end;
Code language: Delphi (delphi)

If you have reached this point, then this huge listing did not scare you, and you can admire the result. The form is implemented as a two-page wizard. On the first page, the user is prompted to read the license agreement (1), which is downloaded from the license.txt file. It should be located next to the executable file, and its absence means a violation of the integrity of the software package and the terms of the license agreement. The “Next” button (3) is unlocked only after the user has accepted the license agreement (2).

The second page contains the input field for the licensor (1) – the owner of the license, the display field for the registration code (2) – the hardware key returned by the App_GetHWKey() function. Next to this field there is a button for copying the registration code to the clipboard, which simplifies the process itself, which involves sending this code to a vendor, distributor or reseller to obtain a license key (4) for this installation of the program.

Registration is completed by clicking the “Activate” button (5), and to get information about where exactly you can buy a license for the program, use the “Buy” button (6), which opens the vendor’s page in the browser.

Summary

Modular licensing has been added to project “Production”. If desired, it can be used in other projects created on the My Visual Database framework. And small improvements will allow using the license.pas module in Delphi projects and other projects using the Object Pascal language.

Leave a Reply

Your email address will not be published. Required fields are marked *