I read a blog article and retook the idea in bellow source code, but I can't find a link to the article now.
You can't make it 100% piracy proof, except if you make online verification with "on-the-fly", asymmetric secret keys exchange (public-private-key), repeatedly (every minute or every request) during all the time the software is been used. This then would mean you'd put a lot of effort in (online) authentication so that the secrets can be exchanged and your license be validated, it only shifts the problem to a more remote place from where you can intervene directly as a developer now. (and it may cost bandwidth plus extra costs for the infra structure) There are private companies that propose those services as third party (beside Steam who'd I consider second party).
Anyway here is what I came up with, it allows for a lot of featuritis & over-engineering
I also wrote a little "GetMachineID" function for all OS, but I couldn't test it on MacOS. This snippet remains in the drawers, but I may publish it.
Code: Select all
; ==============================================================
; === (c) 2024 by benubi ===
; ==============================================================
;
; Source: MakeCDKey.pb
; Thread: -/-
; Author: benubi
; Date: Summer 2024
; OS: All
; Description: Generate and/or check for arbitary "Software Keys" with little nested data, using AES encryption
; UNFINISHED - you can have many options via the flags, I didn't invest too much effort in extra checks like expiration dates & co
;
;
; ============================================================================================================================================================================================================================================================
; ============================================================================================================================================================================================================================================================
; D.O.G. license for MakeCDKey.pb
; Do Only Good with it & be kind to doggo,
; because if you don't he's licensed to bite you.
;
; ^..^ /
; /_/\_____/
; /\ /\
; / \ / \
;
; ============================================================================================================================================================================================================================================================
; ============================================================================================================================================================================================================================================================
; Current nested data is:
;
; Structure CDKEYINFO
; ExpireDate.q ; optional product expiration or activation date
; NonceValue.q ; autokey or serial number of this software key
; CRC32.l ; custom CRC32 value. This MUST be zero before SimpleCheckSum() call is made on the data
; ProductID.u ; you will have to do with max 64k different products for now ;)
; ProductFlags.b ; ExpireDate? DEMO? Professional? Home edition - adapt to your needs
; ProductMajorVersion.a ; 0-255
; ProductMinorVersion.a ; 0-255
; EndStructure
;
; Note: If you change/adapt the CDKEYINFO structure you will have to adapt the Make, Check and Extract procedures.
;
; Currently the last character added in the software key tells about an entry in the secret key table,
; so that there are in fact 32 secret entries inside the "secret key table" that may be
; used by the checker procedures for AES encryption. See inside MakeCDKey().
;
; Adapt #MyCDKeySecret in InitCDKey()
; ============================================================================================================================================================================================================================================================
;
; EnableExplicit
;
; #MakeCDKey=0 ; set to 1 if you want to generate CD keys with this compiled executable
; #CheckCDKey=0 ; set to 1 if you want to check & extract CD key info with this compiled executable
;
Procedure.q ReadBits(*Buffer, offset, amount)
Protected bits_read, *I.BYTE, result.q
*I = *Buffer + (offset / 8)
While bits_read < amount
If *I\b & (1 << (offset & 7))
result | (1 << bits_read)
EndIf
bits_read + 1
offset + 1
If offset & 7 = 0
*I + 1
EndIf
Wend
ProcedureReturn result
EndProcedure
Procedure WriteBits(*Buffer, offset, amount, Bits.q)
Protected bits_written, *O.BYTE
Protected bit
*O = *Buffer + (offset / 8)
While bits_written < amount
bit = (Bits >> bits_written) & $1
If bit
*O\b = *O\b | (1 << (offset & 7))
Else
*O\b = *O\b & ~(1 << (offset & 7))
EndIf
bits_written + 1
offset + 1
If offset & 7 = 0
*O + 1
EndIf
Wend
ProcedureReturn bits_written
EndProcedure
;
Structure CDKEYINFO
ExpireDate.q
NonceValue.q ; autokey
CRC32.l
ProductID.u ;
ProductFlags.b ; ExpireDate? DEMO? Professional? Home edition
ProductMajorVersion.a
ProductMinorVersion.a
EndStructure
;
#CDKEY_CHARS = SizeOf(CDKEYINFO) * 8 / 5 ; 5 = 2^5 aka 32 possible symbols for the software key
;
CompilerIf 1 & $1 = 1 ; LE
Macro LE_WORD(_W_)
(_W_)
EndMacro
Macro LE_DWORD(_L_)
(_L_)
EndMacro
Macro LE_QWORD(_Q_)
(_Q_)
EndMacro
CompilerElse
Macro LE_WORD(_W_)
((_W_ & $FF00) >> 8) | ((_W_ & $FF) << 8)
EndMacro
Macro LE_DWORD(_L_)
((_L_ & $FF000000) >> 24) | ((_L_ & $00FF0000) >> 8) | ((_L_ & $0000FF00) << 8) | ((_L_ & $000000FF) << 24)
EndMacro
Macro LE_QWORD(_Q_)
((_Q_ & $FF000000000000) >> 56) | ((_Q_ & $00FF0000000000) >> 40) | ((_Q_ & $0000FF00000000) >> 24) | ((_Q_ & $000000FF000000) >> 8) | ((_Q_ & $000000FF000000) << 8) | ((_Q_ & $00000000FF0000) << 24) | ((_Q_ & $0000000000FF00) << 40) | ((_Q_ & $000000000000FF) << 56)
EndMacro
CompilerEndIf
;
#CDKEY_MAX_OFFSET = SizeOf(CDKEYINFO) * 8
#CDKEY_VALUES = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" ; 32 unambiguous symbols (e.g. 1 and 0 ommitted that can be confused with L and O etc.)
;
; You may add/change flags but for now there can only be 8 bits for product flags
#CDKF_Trigger_Expire = 1 ;
#CDKF_Unlock_software_on_Expire = 2 ; activation date
#CDKF_Lock_On_Expire = 4 ; expiration date
#CDKF_Free_Major_Upgrades = 8 ; your app version may be higher than the major version in the nested data
#CDKF_Multi_Users = 16 ; there can me multiple copies of this key
#CDKF_Beta_Version = 128 ; this is a beta software version key
;
Procedure.l SimpleCheckSum(*Buff, length) ; Custom CRC32
Protected q1.l = -721483648 ; IDK if it's good. Change at will
Protected pos, *B.byte = *Buff
While pos < length
q1 * q1
q1 ! (*B\b * pos)
pos + 1
Wend
ProcedureReturn q1
EndProcedure
;
CompilerIf Not Defined(MakeCDKey, #PB_Constant)
#MakeCDKey = 1 ; Compile with cd key maker procedure
CompilerEndIf
CompilerIf Not Defined(CheckCDKey, #PB_Constant)
#CheckCDKey = 1 ; Compile with check & extract procedures
CompilerEndIf
;
DataSection
key_db_start:
; This will be filled with pseudo-random data or your secret key
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
Data.q 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
;IncludeBinary "perhaps_my_static_company_logo_is_a_secret_key_table.png"
key_db_end: ;
EndDataSection
;
CompilerIf #MakeCDKey
; Make
Procedure$ MakeCDKey(ProductID, ProductFlags, MajorVersion, MinorVersion, NonceValue.q, ExpireDate.q) ; The product ID has to be hard-coded into the SW key checking application, too.
Protected temp.CDKEYINFO
Protected temp2.CDKEYINFO
Protected result$
Protected i
Protected rNum = NonceValue & 31
Protected Key_Size_Pad = (?key_db_end - ?key_db_start) / 32
Protected *Key = ?key_db_start + (rNum * Key_Size_Pad)
Protected *Vec = ?key_db_end - ( (rnum + 1) *Key_Size_Pad)
; set info values
temp\ProductID = LE_WORD(ProductID)
temp\ProductFlags = LE_WORD(ProductFlags)
temp\ProductMajorVersion = MajorVersion
temp\ProductMinorVersion = MinorVersion
temp\NonceValue = LE_DWORD(NonceValue)
temp\ExpireDate = LE_QWORD(ExpireDate)
temp\CRC32 = 0
; make checksum
Protected checksum = SimpleCheckSum(@temp, SizeOf(CDKEYINFO))
temp\CRC32 = LE_DWORD(checksum) ;
AESEncoder(@temp, @temp2, SizeOf(CDKEYINFO), *key, 128, *vec)
While i * 5 < #CDKEY_MAX_OFFSET
result$ + Mid(#CDKEY_VALUES, 1 + ReadBits(@temp2, 5 * i, 5), 1)
If i % 5 = 4
result$ + "-"
EndIf
i + 1
Wend
If Right(result$, 1) = "-"
result$ = Left(result$, Len(Result$) - 1)
EndIf
result$ + Mid(#CDKEY_VALUES, 1 + rNum, 1)
;result$ = Left(result$, 34)
ProcedureReturn result$
EndProcedure
CompilerEndIf
;
; check / extract
;
CompilerIf #CheckCDKey
Procedure CheckCDKey(CDKey$, ProductID, CurrentDate.q) ; Product ID is hard-coded into your program
CDKey$ = Trim(CDKey$)
Protected temp.CDKEYINFO
Protected temp2.CDKEYINFO
Protected *C.character = @CDKey$
Protected offset = 0
Protected t.q
Protected result
Protected rNum = FindString(#CDKEY_VALUES, Right(CDKey$, 1)) - 1
Protected Key_Size_Pad = (?key_db_end - ?key_db_start) / 32
Protected *Key = ?key_db_start + (rNum * Key_Size_Pad)
Protected *Vec = ?key_db_end - ( (rnum + 1) *Key_Size_Pad)
If rnum = -1
ProcedureReturn 0
EndIf
While *C\c And offset < #CDKEY_MAX_OFFSET
t = FindString(#CDKEY_VALUES, Chr(*C\c))
If t
WriteBits(@temp, offset, 5, t - 1)
offset + 5
EndIf
*C + SizeOf(Character)
Wend
AESDecoder(@temp, @temp2, SizeOf(CDKEYINFO), *Key, 128, *Vec)
Protected checksum = (temp2\CRC32)
checksum = LE_DWORD(checksum)
temp2\CRC32 = 0
If SimpleCheckSum(@temp2, SizeOf(CDKEYINFO)) = checksum
result | 1
; Debug "checksum ok"
If LE_WORD(temp2\ProductID) = LE_WORD(ProductID)
result | 2
;Debug "product id ok"
EndIf
If temp2\ExpireDate <= 0 Or temp2\ExpireDate > CurrentDate
; Debug "expire date ok"
result | 4
EndIf
EndIf
ProcedureReturn result ; 31 = all checks succeeded ??
EndProcedure
Procedure ExtractCDKeyInfo(CDKey$, *Info.CDKEYINFO) ; returns non-zero if the extracted data is valid
CDKey$ = Trim(CDKey$)
Protected temp.CDKEYINFO
Protected temp2.CDKEYINFO
Protected *C.character = @CDKey$
Protected offset = 0
Protected t.q
Protected result
Protected rNum = FindString(#CDKEY_VALUES, Right(CDKey$, 1)) - 1
Protected Key_Size_Pad = (?key_db_end - ?key_db_start) / 32
Protected *Key = ?key_db_start + (rNum * Key_Size_Pad)
Protected *Vec = ?key_db_end - ( (rnum + 1) *Key_Size_Pad)
If rnum = -1
ProcedureReturn 0
EndIf
While *C\c And offset < #CDKEY_MAX_OFFSET
t = FindString(#CDKEY_VALUES, Chr(*C\c))
If t
WriteBits(@temp, offset, 5, t - 1)
offset + 5
EndIf
*C + SizeOf(Character)
Wend
AESDecoder(@temp, @temp2, SizeOf(CDKEYINFO), *Key, 128, *Vec)
Protected checksum = (temp2\CRC32)
checksum = LE_DWORD(checksum)
temp2\CRC32 = 0
If SimpleCheckSum(@temp2, SizeOf(CDKEYINFO)) = checksum
result | 1
If *Info
*Info\ProductID = LE_DWORD( temp2\ProductID )
*Info\ProductFlags = LE_WORD(temp2\ProductFlags)
*Info\ProductMajorVersion = temp2\ProductMajorVersion
*Info\ProductMinorVersion = temp2\ProductMinorVersion
*Info\NonceValue = LE_QWORD(temp2\NonceValue)
*Info\ExpireDate = LE_QWORD(temp2\ExpireDate)
*Info\CRC32 = checksum
EndIf
EndIf
ProcedureReturn result ; 31 = all checks succeeded
EndProcedure
CompilerEndIf
;
Procedure InitCDKey() ; This procedure has to be present in key maker and checker. Adapt random values.
UseMD5Fingerprint()
Define oldrandom = Random($7FFFFFFF)
#MyCDKeySecret = 20050329 ; the date I joined the english speaking forum, of course ;o)
RandomSeed(#MyCDKeySecret)
RandomData(?key_db_Start, ?key_db_end - ?key_db_start)
RandomSeed(oldrandom)
EndProcedure
; ----------------- DEMO --- MAKE & CHECK CD KEYS ------------------------
CompilerIf #PB_Compiler_IsMainFile
;
DisableExplicit
;
InitCDKey()
;
remember$ = ""
;
For i = 10001 To 10100 ; serial numbers / auto key
key$ = MakeCDKey(1001, 23, 2, 0, i, -1) ;
Debug LSet(Str(i) + ".", 5) + " " + key$ + ": " + StringField("BAD|OK", 1 + (1*Bool(CheckCDKey(key$, 1001, -1))), "|")
If Random(10) > 9
remember$ = key$
EndIf
Next
;
Debug "secret AES key length (table):"
Debug ?key_db_end - ?key_db_start
;
result$ = MakeCDKey(1001, 23, 2, 0, 10000, -1)
;
Debug "CD-Code length (without dash): " + Len(RemoveString(Result$, "-"))
Debug "CD Key info structure size (guessed): " + StrD( - 1 + Len(RemoveString(Result$, "-")) * 5 / 8, 2)
Debug "CD Key info structure size: " + SizeOf(CDKEYINFO)
Debug "CDKEY : " + result$
Debug "Check CD Key: " + Str(CheckCDKey(result$, 1001, 0))
;
Define myinfo.CDKEYINFO
;
Debug "Remembered key: " + remember$
;
If ExtractCDKeyInfo(remember$ + remember$, @myinfo)
Debug "Product ID: " + myinfo\ProductID
Debug "Product flags: " + myinfo\ProductFlags
Debug "Major version: " + myinfo\ProductMajorVersion
Debug "Minor version: " + myinfo\ProductMinorVersion
Debug "Nonce value: " + myinfo\NonceValue
Debug "Expir. date: " + myinfo\ExpireDate
Debug "checksum: " + myinfo\CRC32
Else
Debug "ExtractCDKey failed"
EndIf
;
CompilerEndIf