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 type | Composition of an expression | Comment |
---|---|---|
Lifetime | Hardware key + secret word | |
Versioned | Hardware key + secret word + version number | The major version number is used, the license is valid for all sub-versions of the software |
Modular | Hardware key + secret word + module prefix + module list | The module list is a comma-separated list of module numbers |
Versioned modular | Hardware 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.
Processes | Visual constructor | Program code |
---|---|---|
Creation | Fast, immediate results | Slow, application needs to be launched to view results |
Editing | Fast | Slow |
Transfer to other projects | Requires 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.