unit ULicenseManager; {$mode objfpc}{$H+} interface uses SysUtils, Classes, IniFiles, TypInfo, DateUtils, StrUtils, OnGuard, OGUtil, MyEventLogging; type TLicenseActivationMethod = ( lamUnknown, lamDate, lamDays, lamRegistration, lamSerialNumber, lamUsage, lamSpecial ); TCodeStatusSet = set of TCodeStatus; { TLicenseManager encapsulates the license management and validation logic. It: • Prepares the decryption key using ProductSecretKey (and an optional machine modifier) • Converts a license hex string into a TCode buffer • Detects the license code type and applies validation (expiry, usage/day limits, etc.) • Builds a detailed, human‑readable validation report with friendly text and emojis • Applies forced status corrections (manual promotion/demotion) • Merges status flags to yield a final valid/invalid decision • For volatile codes (ctUsage and ctDays), decrements the corresponding counter and updates storage • Prevents reactivation by checking/updating the BlockedLicenses list IMPORTANT: The ActivateLicense method returns its detailed validation report via the OUT parameter. } TLicenseManager = class(TComponent) private FPreparedKey: TKey; FLicenseCode: string; FStatusCode: TCodeStatus; FValidationDetails: string; FCodeBuffer: TCode; FValidationStatuses: string; function RemoveFirstEmptyLine(const S: string): string; function IsLicenseBlocked(const LicenseCode: string): Boolean; procedure BlockLicense(const LicenseCode: string); public constructor Create(AOwner: TComponent); override; function ActivateLicense(const LicenseCode, MachineCode: string; out ValidationDetails: string): Boolean; function GetLicenseActivationMethod(const LicenseCode, MachineCode: string): TLicenseActivationMethod; property ValidationDetails: string read FValidationDetails; end; { Global helper routines – declared for full visibility } function ReadLicenseCode: string; procedure WriteLicenseCode(const ACode: string); function GetMachineModifierString: string; function StatusToString(Status: TCodeStatus): string; function CodeTypeToHumanReadableString(CodeType: TCodeType): string; function CustomStrToDateDef(const S: string; const Default: TDateTime): TDateTime; function ReadLastDecrementDate(const StableKey: string): TDateTime; procedure WriteLastDecrementDate(const StableKey: string; ADate: TDateTime); function ReadOriginalLicenseKey: string; function DecDaysCode(const Key: TKey; var Code: TCode; const StableKey: string): Boolean; procedure DecUsageCode(const Key: TKey; var Code: TCode); const ProductSecretKey: TKey = ( $C0, $D7, $FE, $88, $05, $23, $20, $F2, $80, $E3, $84, $3F, $0F, $53, $E6, $CA ); ShortDateFormat = 'yyyy-mm-dd'; implementation {============================================================================== Global Helper Functions ==============================================================================} function ReadLicenseCode: string; var Ini: TIniFile; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try Result := Ini.ReadString('License', 'Key', ''); DebugLog('📥 ReadLicenseCode: ' + Result); finally Ini.Free; end; end; procedure WriteLicenseCode(const ACode: string); var Ini: TIniFile; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try Ini.WriteString('License', 'Key', ACode); Ini.UpdateFile; DebugLog('📤 WriteLicenseCode: ' + ACode); finally Ini.Free; end; end; function GetMachineModifierString: string; var Modifier: LongInt; begin Modifier := GenerateMachineModifierPrim; Result := '$' + BufferToHex(Modifier, SizeOf(Modifier)); DebugLog('🔐 GetMachineModifierString: ' + Result); end; function StatusToString(Status: TCodeStatus): string; begin case Status of ogValidCode: Result := '✅ Valid'; ogInvalidCode: Result := '❌ Invalid'; ogPastEndDate: Result := '⌛ Past End Date'; ogDayCountUsed: Result := '⏱ Days Used Up'; ogRunCountUsed: Result := '🔄 Run Count Exceeded'; ogNetCountUsed: Result := '🚫 Network Limit Exceeded'; ogCodeExpired: Result := '🛑 Code Expired'; else Result := 'Unknown'; end; end; function CodeTypeToHumanReadableString(CodeType: TCodeType): string; begin case CodeType of ctDate: Result := '📅 Date Code'; ctDays: Result := '⏰ Days Code'; ctRegistration: Result := '📝 Registration Code'; ctSerialNumber: Result := '🔒 Serial Number Code'; ctUsage: Result := '🔢 Usage Code'; ctSpecial: Result := '⭐ Special License'; else Result := 'Unknown License Type'; end; end; function CustomStrToDateDef(const S: string; const Default: TDateTime): TDateTime; var Y, M, D: Word; begin if Length(S) >= 10 then begin try Y := StrToInt(Copy(S, 1, 4)); M := StrToInt(Copy(S, 6, 2)); D := StrToInt(Copy(S, 9, 2)); Result := EncodeDate(Y, M, D); except Result := Default; end; end else Result := Default; end; function ReadLastDecrementDate(const StableKey: string): TDateTime; var Ini: TIniFile; s: string; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try s := Ini.ReadString('LastActivation', StableKey, ''); DebugLog('ReadLastDecrementDate for key ' + StableKey + ': value = ' + s); if s = '' then Result := 0 else Result := CustomStrToDateDef(s, 0); finally Ini.Free; end; end; procedure WriteLastDecrementDate(const StableKey: string; ADate: TDateTime); var Ini: TIniFile; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try Ini.WriteString('LastActivation', StableKey, FormatDateTime(ShortDateFormat, ADate)); Ini.UpdateFile; DebugLog('WriteLastDecrementDate for key ' + StableKey + ': value = ' + FormatDateTime(ShortDateFormat, ADate)); finally Ini.Free; end; end; function ReadOriginalLicenseKey: string; var Ini: TIniFile; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try Result := Ini.ReadString('License', 'LicenseKey', ''); finally Ini.Free; end; end; {-------------------------------------------------------------------------- DecDaysCode now returns True if the day count was actually decremented. Comparison of dates is done on the truncated (whole day) values. --------------------------------------------------------------------------} function DecDaysCode(const Key: TKey; var Code: TCode; const StableKey: string): Boolean; var TodayWord: word; LastDate: TDateTime; begin LastDate := ReadLastDecrementDate(StableKey); if (LastDate <> 0) and (Trunc(LastDate) = Trunc(Date)) then begin DebugLog('No day decrement needed (activation within same day).'); Result := False; Exit; end; MixBlock(T128Bit(Key), Code, False); TodayWord := ShrinkDate(Date); if Code.Days > 0 then Code.Days := Code.Days - 1; Code.LastAccess := TodayWord; MixBlock(T128Bit(Key), Code, True); { Write the truncated date so that only the day portion is stored } WriteLastDecrementDate(StableKey, Trunc(Date)); DebugLog('⏰ Updated Day Count: ' + IntToStr(Code.Days)); Result := True; end; procedure DecUsageCode(const Key: TKey; var Code: TCode); var TodayWord: word; begin MixBlock(T128Bit(Key), Code, False); if Code.UsageCount > 0 then Code.UsageCount := Code.UsageCount - 1; TodayWord := ShrinkDate(Date); if Code.LastChange < TodayWord then Code.LastChange := TodayWord; MixBlock(T128Bit(Key), Code, True); DebugLog('🔢 Updated Usage Count: ' + IntToStr(Code.UsageCount)); end; {============================================================================== TLicenseManager Implementation ==============================================================================} constructor TLicenseManager.Create(AOwner: TComponent); begin inherited Create(AOwner); FPreparedKey := ProductSecretKey; FLicenseCode := ''; FStatusCode := ogInvalidCode; FValidationDetails := ''; FValidationStatuses := ''; FillChar(FCodeBuffer, SizeOf(FCodeBuffer), 0); end; function TLicenseManager.RemoveFirstEmptyLine(const S: string): string; var Lines: TStringList; begin Lines := TStringList.Create; try Lines.Text := S; while (Lines.Count > 0) and (Trim(Lines[0]) = '') do Lines.Delete(0); Result := Lines.Text; while (Length(Result) >= Length(sLineBreak)) and (Copy(Result, 1, Length(sLineBreak)) = sLineBreak) do Delete(Result, 1, Length(sLineBreak)); finally Lines.Free; end; end; function TLicenseManager.IsLicenseBlocked(const LicenseCode: string): Boolean; var Ini: TIniFile; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try Result := Ini.ReadString('BlockedLicenses', LicenseCode, '') <> ''; finally Ini.Free; end; end; procedure TLicenseManager.BlockLicense(const LicenseCode: string); var Ini: TIniFile; begin Ini := TIniFile.Create(GetAppConfigFile(False)); try Ini.WriteString('BlockedLicenses', LicenseCode, DateTimeToStr(Now)); Ini.UpdateFile; DebugLog('🚫 Blocked license code: ' + LicenseCode); finally Ini.Free; end; end; function TLicenseManager.ActivateLicense(const LicenseCode, MachineCode: string; out ValidationDetails: string): Boolean; var DecryptionKey: TKey; Modifier: LongInt; CodeType, StoredCodeType: TCodeType; InitialStatus: TCodeStatus; CombinedStatus: TCodeStatusSet; StatusesString: string; dtRaw, dtAdj: TDateTime; cs: TCodeStatus; FinalDecisionString: string; remainingDays, remainingUsage: integer; Decremented: Boolean; CurrentStoredCode: string; TempBuffer: TCode; StableKey: string; begin Result := False; FValidationDetails := ''; FinalDecisionString := ''; DebugLog('==== ActivateLicense START ==== 😃'); DebugLog('LicenseCode (provided): ' + LicenseCode); DebugLog('MachineCode: ' + MachineCode); // Reject empty license code if LicenseCode = '' then begin DebugLog('❌ ERROR: Empty license code provided'); FValidationDetails := '❌ Empty license code provided'; ValidationDetails := FValidationDetails; Exit(False); end; // Step 1: Exit immediately if provided code is blocked. if IsLicenseBlocked(LicenseCode) then begin DebugLog('License code is already blocked.'); FValidationDetails := 'License code is blocked.'; ValidationDetails := FValidationDetails; Exit(False); end; // Step 2: Read current stored license. CurrentStoredCode := ReadLicenseCode; DebugLog('Current stored license code: ' + CurrentStoredCode); // Step 3: If a stored license exists, try decoding it. if CurrentStoredCode <> '' then begin if HexToBuffer(CurrentStoredCode, TempBuffer, SizeOf(TempBuffer)) then begin StoredCodeType := GetCodeType(FPreparedKey, TempBuffer); DebugLog('Stored license type: ' + CodeTypeToHumanReadableString(StoredCodeType)); { If the stored license type is volatile, then we enforce that the last (active) volatile code must match our new code to be considered a reactivation. Otherwise, if the stored type is nonvolatile (ctSerialNumber, ctRegistration, etc.), we let the new code be processed independently. } if (StoredCodeType in [ctDays, ctUsage]) then begin if LicenseCode <> CurrentStoredCode then begin DebugLog('Outdated volatile license code provided; blocking it.'); BlockLicense(LicenseCode); FValidationDetails := 'This volatile license code is outdated and has been blocked.'; ValidationDetails := FValidationDetails; Exit(False); end else begin DebugLog('Reactivation using current stored volatile license code.'); Result := True; FValidationDetails := 'License reactivated; no further decrement performed today.'; ValidationDetails := FValidationDetails; Exit(True); end; end; // If stored type is nonvolatile, do not block the new code. end else DebugLog('Stored license code could not be decoded; proceeding with new code.'); end; // Step 4: Assign provided code and prepare decryption key. FLicenseCode := LicenseCode; DecryptionKey := FPreparedKey; // Step 5: Apply machine modifier if provided. if Trim(MachineCode) <> '' then begin if not TryStrToInt(MachineCode, Modifier) then begin DebugLog('❌ ERROR: Invalid machine code format'); FValidationDetails := '❌ Invalid machine code format'; ValidationDetails := FValidationDetails; Exit(False); end; Modifier := SwapEndian(Modifier); DebugLog('🔧 Using swapped machine modifier: $' + IntToHex(Modifier, 8)); ApplyModifierToKeyPrim(Modifier, DecryptionKey, SizeOf(DecryptionKey)); end; // Step 6: Decode the provided license code. if not HexToBuffer(LicenseCode, FCodeBuffer, SizeOf(FCodeBuffer)) then begin DebugLog('❌ ERROR: Invalid license code hex format'); FValidationDetails := '❌ Invalid license code format'; ValidationDetails := FValidationDetails; Exit(False); end; // Step 7: Detect the code type of the new code. CodeType := GetCodeType(DecryptionKey, FCodeBuffer); DebugLog('Detected CodeType (new code): ' + GetEnumName(TypeInfo(TCodeType), Ord(CodeType))); FValidationDetails := 'License Code Type: ' + CodeTypeToHumanReadableString(CodeType) + sLineBreak; // (Optional) If a stored license exists but its type is nonvolatile, we ignore it. // [No extra check needed here because we cleared volatile stored code above.] // Step 8: Validate the provided license code based on its type. case CodeType of ctDate: begin if IsDateCodeValid(DecryptionKey, FCodeBuffer) then InitialStatus := ogValidCode else InitialStatus := ogCodeExpired; if IsDateCodeExpired(DecryptionKey, FCodeBuffer) then InitialStatus := ogCodeExpired; end; ctDays: begin if IsDaysCodeValid(DecryptionKey, FCodeBuffer) then InitialStatus := ogValidCode else InitialStatus := ogInvalidCode; if IsDaysCodeExpired(DecryptionKey, FCodeBuffer) then InitialStatus := ogCodeExpired; end; ctRegistration: begin if IsRegCodeValid(DecryptionKey, FCodeBuffer) then InitialStatus := ogValidCode else InitialStatus := ogInvalidCode; if GetExpirationDate(DecryptionKey, FCodeBuffer) < Date then InitialStatus := ogCodeExpired; end; ctSerialNumber: begin if IsSerialNumberCodeValid(DecryptionKey, FCodeBuffer) then InitialStatus := ogValidCode else InitialStatus := ogInvalidCode; if GetExpirationDate(DecryptionKey, FCodeBuffer) < Date then InitialStatus := ogCodeExpired; end; ctUsage: begin if IsUsageCodeValid(DecryptionKey, FCodeBuffer) then InitialStatus := ogValidCode else InitialStatus := ogInvalidCode; if IsUsageCodeExpired(DecryptionKey, FCodeBuffer) then InitialStatus := ogCodeExpired; end; ctSpecial: begin if IsSpecialCodeValid(DecryptionKey, FCodeBuffer) then InitialStatus := ogValidCode else InitialStatus := ogInvalidCode; if GetExpirationDate(DecryptionKey, FCodeBuffer) < Date then InitialStatus := ogCodeExpired; end; else begin DebugLog('❌ ERROR: Unsupported CodeType'); FValidationDetails := '❌ Unsupported license type'; ValidationDetails := FValidationDetails; Exit(False); end; end; FStatusCode := InitialStatus; // Step 9: Build detailed validation report. case CodeType of ctDate: begin dtRaw := GetDateCodeValue(DecryptionKey, FCodeBuffer); DebugLog('🕒 Raw Expiration Date (Date Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtRaw)); if dtRaw < EncodeDate(2000, 1, 1) then dtAdj := IncYear(dtRaw, 96) + 1 else dtAdj := dtRaw; DebugLog('🕒 Adjusted Expiration Date (Date Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtAdj)); FValidationDetails := FValidationDetails + Format( '📅 Date Code%sExpiration Date: %s%sCurrent Date: %s', [sLineBreak, FormatDateTime(ShortDateFormat, dtAdj), sLineBreak, FormatDateTime(ShortDateFormat, Date)]); end; ctDays: begin dtRaw := GetExpirationDate(DecryptionKey, FCodeBuffer); DebugLog('🕒 Raw Expiry Date (Days Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtRaw)); if dtRaw < EncodeDate(2000, 1, 1) then dtAdj := IncYear(dtRaw, 96) + 1 else dtAdj := dtRaw; DebugLog('🕒 Adjusted Expiry Date (Days Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtAdj)); remainingDays := GetDaysCodeValue(DecryptionKey, FCodeBuffer); FValidationDetails := FValidationDetails + Format( '⏰ Days Code%sDays Remaining: %d%sExpiry Date: %s', [sLineBreak, remainingDays, sLineBreak, FormatDateTime(ShortDateFormat, dtAdj)]); if remainingDays <= 0 then begin DebugLog('❌ Day limit exhausted!'); FValidationDetails := FValidationDetails + sLineBreak + '❌ Day limit exhausted!'; FStatusCode := ogCodeExpired; end; end; ctUsage: begin dtRaw := GetExpirationDate(DecryptionKey, FCodeBuffer); DebugLog('🕒 Raw Expiry Date (Usage Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtRaw)); if dtRaw < EncodeDate(2000, 1, 1) then dtAdj := IncYear(dtRaw, 96) + 1 else dtAdj := dtRaw; DebugLog('🕒 Adjusted Expiry Date (Usage Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtAdj)); remainingUsage := GetUsageCodeValue(DecryptionKey, FCodeBuffer); FValidationDetails := FValidationDetails + Format( '🔢 Usage Code%sUses Remaining: %d%sExpiry Date: %s', [sLineBreak, remainingUsage, sLineBreak, FormatDateTime(ShortDateFormat, dtAdj)]); if remainingUsage <= 0 then begin DebugLog('❌ Usage limit exhausted!'); FValidationDetails := FValidationDetails + sLineBreak + '❌ Usage limit exhausted!'; FStatusCode := ogCodeExpired; end; end; ctRegistration, ctSerialNumber, ctSpecial: begin dtRaw := GetExpirationDate(DecryptionKey, FCodeBuffer); DebugLog('🕒 Raw Expiry Date (Nonvolatile Code): ' + FormatDateTime('yyyy-mm-dd hh:nn:ss', dtRaw)); if dtRaw < EncodeDate(2000, 1, 1) then dtAdj := IncYear(dtRaw, 96) + 1 else dtAdj := dtRaw; DebugLog('🕒 Adjusted Expiry Date (Nonvolatile Code): ' + FormatDateTime(ShortDateFormat, dtAdj)); if dtAdj = EncodeDate(9999, 12, 31) then FValidationDetails := FValidationDetails + 'Permanent License' else FValidationDetails := FValidationDetails + '📝 Expiry Date: ' + FormatDateTime(ShortDateFormat, dtAdj); end; end; if dtAdj < Date then FValidationDetails := FValidationDetails + sLineBreak + '⚠️ License EXPIRED!'; if (dtAdj < Date) and (dtAdj <> dtRaw) and (FStatusCode = ogValidCode) then begin DebugLog('🔻 Manually demoting license status.'); FStatusCode := ogCodeExpired; end; if (dtAdj >= Date) and (dtAdj <> dtRaw) and ((FStatusCode = ogPastEndDate) or (FStatusCode = ogCodeExpired)) and not ((CodeType = ctUsage) and (remainingUsage <= 0)) and not ((CodeType = ctDays) and (remainingDays <= 0)) then begin DebugLog('🔺 Manually promoting license status.'); FStatusCode := ogValidCode; end; CombinedStatus := [FStatusCode, InitialStatus]; StatusesString := ''; for cs := Low(TCodeStatus) to High(TCodeStatus) do if cs in CombinedStatus then StatusesString := StatusesString + StatusToString(cs) + ', '; if StatusesString <> '' then Delete(StatusesString, Length(StatusesString) - 1, 2); DebugLog('🧩 Combined statuses: ' + StatusesString); if (ogValidCode in CombinedStatus) and (not (ogInvalidCode in CombinedStatus)) and (not (ogDayCountUsed in CombinedStatus)) and (not (ogRunCountUsed in CombinedStatus)) and (not (ogNetCountUsed in CombinedStatus)) and (FStatusCode = ogValidCode) then begin Result := True; FinalDecisionString := '✅ LICENSE IS VALID (TRUE)'; DebugLog('Final Decision: ' + FinalDecisionString); end else begin Result := False; FinalDecisionString := '❌ LICENSE IS NOT VALID (FALSE)'; DebugLog('Final Decision: ' + FinalDecisionString); end; FValidationDetails := FValidationDetails + sLineBreak + FinalDecisionString; FValidationDetails := RemoveFirstEmptyLine(FValidationDetails); ValidationDetails := FValidationDetails; // Blocking logic: // - For ctDate and nonvolatile (ctRegistration, ctSerialNumber, ctSpecial): block if expired. // - For ctUsage, block immediately after consumption. // - For ctDays, block only if expired. if CodeType = ctDate then begin if (FStatusCode = ogCodeExpired) or (FStatusCode = ogPastEndDate) then BlockLicense(LicenseCode); end else if (CodeType = ctRegistration) or (CodeType = ctSerialNumber) or (CodeType = ctSpecial) then begin if FStatusCode = ogCodeExpired then BlockLicense(LicenseCode); end else if CodeType = ctUsage then BlockLicense(LicenseCode) else if CodeType = ctDays then begin if FStatusCode = ogCodeExpired then BlockLicense(LicenseCode); end; // Update stored license for volatile types. if CodeType in [ctDays, ctUsage] then begin if CodeType = ctUsage then begin DecUsageCode(DecryptionKey, FCodeBuffer); DebugLog('🔢 Usage count decremented.'); WriteLicenseCode(BufferToHex(FCodeBuffer, SizeOf(FCodeBuffer))); end else if CodeType = ctDays then begin StableKey := 'DaysActivation_' + ReadOriginalLicenseKey; Decremented := DecDaysCode(DecryptionKey, FCodeBuffer, StableKey); if Decremented then begin DebugLog('⏰ Day count decremented.'); WriteLicenseCode(BufferToHex(FCodeBuffer, SizeOf(FCodeBuffer))); end else begin DebugLog('Day count not decremented – activation already occurred today. License remains active.'); end; end; end else if CodeType in [ctDate, ctRegistration, ctSerialNumber, ctSpecial] then begin WriteLicenseCode(LicenseCode); DebugLog('📌 Nonvolatile license validated; code unchanged.'); end; DebugLog('==== ActivateLicense END ==== 😃'); end; function TLicenseManager.GetLicenseActivationMethod(const LicenseCode, MachineCode: string): TLicenseActivationMethod; var CodeBuffer: TCode; DecryptionKey: TKey; Modifier: LongInt; begin Result := lamUnknown; if HexToBuffer(LicenseCode, CodeBuffer, SizeOf(CodeBuffer)) then begin DecryptionKey := FPreparedKey; if Trim(MachineCode) <> '' then if TryStrToInt(MachineCode, Modifier) then begin Modifier := SwapEndian(Modifier); ApplyModifierToKeyPrim(Modifier, DecryptionKey, SizeOf(DecryptionKey)); end; case GetCodeType(DecryptionKey, CodeBuffer) of ctDate: Result := lamDate; ctDays: Result := lamDays; ctRegistration: Result := lamRegistration; ctSerialNumber: Result := lamSerialNumber; ctUsage: Result := lamUsage; ctSpecial: Result := lamSpecial; else Result := lamUnknown; end; end; DebugLog('License Activation Method: ' + CodeTypeToHumanReadableString(GetCodeType(FPreparedKey, FCodeBuffer))); end; initialization DebugLog('==== License Manager Initialized ==== 😀'); DebugLog('Product Secret: ' + BufferToHex(ProductSecretKey, SizeOf(ProductSecretKey))); finalization DebugLog('==== License Manager Shutdown ==== 😃'); end.