Implementing a Partial Serial Number Verification System in Delphi

Most micro-ISVs use a serial number/registration code system to allow end users to unlock or activate their purchase.  The problem most of us have run into is that a few days or weeks after our software is released, someone has developed a keygen, a crack, or has leaked a serial number across the internet.

There are several possible solutions to this problem. You could license a system like Armadillo/Software Passport or ASProtect, or you could distribute a separate full version as a download for your paying customers. Each option has advantages and disadvantages. What I am going to show you is a way to keep “rolling your own” license key system while making working cracks harder for crackers to produce, and working keygens a thing of the past.

Aside: If you think it’s crazy to post this publicly where crackers can see it, don’t worry about that. I’m not posting anything they haven’t seen before. The entire point of partial key verification is that your code never includes enough information to reverse engineer a key generation algorithm. Also, I offer no warranty of any kind — this is for your information only! Now, on with things.

Our license key system must meet some basic requirements.

  1. License keys must be easy enough to type in.
  2. We must be able to blacklist (revoke) a license key in the case of chargebacks or purchases with stolen credit cards.
  3. No “phoning home” to test keys.  Although this practice is becoming more and more prevalent, I still do not appreciate it as a user, so will not ask my users to put up with it.
  4. It should not be possible for a cracker to disassemble our released application and produce a working “keygen” from it. This means that our application will not fully test a key for verification. Only some of the key is to be tested. Further, each release of the application should test a different portion of the key, so that a phony key based on an earlier release will not work on a later release of our software.
  5. Important: it should not be possible for a legitimate user to accidentally type in an invalid key that will appear to work but fail on a future version due to a typographical error.

The solution is called a Partial Key Verification System because your software never tests the full key. Since your application does not include the code to test every portion of the key, it is impossible for a cracker to build a working valid key generator just by disassembling your executable code.

This system is not a way to prevent cracks entirely. It will still be possible for a cracker to edit your executable to jump over verification code. But such cracks only work on one specific release, and I’ll suggest a couple of tricks to make their job harder to complete successfully.

Let’s jump right in.  I’ll show you the system with Delphi code. (Given the readable nature of Delphi Pascal, you should be able to use these examples to build your own system in any language.)

An aside: if you think it’s crazy to post this publicly where crackers can see it, don’t worry! I’m not posting anything they don’t know. The whole point of this system is that your code never contains enough information for a cracker to reverse-engineer your key system. My blog post here doesn’t give them any information they don’t already have. Also, I’m not offering any kind of warranty with this information. This is for your information only, and all that. Now, on with things!

1. The Key Format

This example will create and test keys of 20 characters (with hyphens added for user convenience). A valid key will look like this:
A279-1717-7D7A-CA2E-7154

Once the hyphens are stripped, this is how the key breaks down:

Seed value

Key Byte 0

Key Byte 1

Key Byte 2

Key Byte 3

Checksum

A2791717

7D

7A

CA

2E

7154

This sample system only uses four bytes for key verification, but a real system should use many more and larger values, so keep that in mind if you begin implementing your own PKVS.

2. The Seed Value

This sample system uses a 64-bit integer as a “seed” to generate the “Key Bytes” from.  The example project just generates random values for seeds, but when you implement a system like this, you must ensure that the seeds are always unique, because the seed is used when blacklisting a key. The seed could itself be a hash of a user name and time of generation, or any number of things

3. Computing Key Bytes

Here is the heart of the PKVS. Each “byte” of the key is a result of an operation on the seed value.  Here is a simple “byte” value computation function. It performs some bit twiddling based on the supplied parameters:

function PKV_GetKeyByte(const Seed : Int64; a, b, c : Byte) : Byte;
begin
  a := a mod 25;
  b := b mod 3;
  if a mod 2 = 0 then
    result := ((Seed shr a) and $000000FF) xor ((Seed shr b) or c)
  else
    result := ((Seed shr a) and $000000FF) xor ((Seed shr b) and c);
end;

We’ll see in a moment how this function is used in the key generation and checking algorithms. Please keep in mind that this example function is very simplistic. A more effective function would use larger values than bytes and employ a more complex hashing system.

4. We need a checksum

Once we have our seed and bytes formed into a string of characters, we need to add a checksum to it. This way we can know when a user makes a mistake entering their key, without having to actually check each portion of the key for validity.

function PKV_GetChecksum(const s : String) : String;
var
  left, right, sum : Word;
  i : Integer;
begin
  left := $0056;
  right := $00AF;
  if Length(s) > 0 then
    for i := 1 to Length(s) do
    begin
      right := right + Byte(s[i]);
      if right > $00FF then
        Dec(right, $00FF);
      Inc(left, right);
      if left > $00FF then
        Dec(left, $00FF);
    end;
  sum := (left shl 8) + right;
  result := IntToHex(sum, 4);
end;

This function computes a simple 8-bit value from the supplied string and returns it as a hexidecimal string, which we tack on to the end of our key.

Note that because this routine is always used to check a key in your application, a would-be keygen coder will be able to generate keys that pass the checksum test.  That’s okay.  The point of the checksum is only to prevent your users from mistyping their own valid license keys, and it will aid in determining if a key was deliberately forged.

5. Putting it together: generating a valid key

For our key generation program, we’re going to need a single function we can call to get a license key string from a seed value. Here it is:

function PKV_MakeKey(const Seed : Int64) : String;
var
  KeyBytes : array[0..3] of Byte;
  i : Integer;
begin
  // Fill KeyBytes with values derived from Seed.
  // The parameters used here must be extactly the same
  // as the ones used in the PKV_CheckKey function.
  // A real key system should use more than four bytes.
  KeyBytes[0] := PKV_GetKeyByte(Seed, 24, 3, 200);
  KeyBytes[1] := PKV_GetKeyByte(Seed, 10, 0, 56);
  KeyBytes[2] := PKV_GetKeyByte(Seed, 1, 2, 91);
  KeyBytes[3] := PKV_GetKeyByte(Seed, 7, 1, 100);
   // the key string begins with a hexidecimal string of the seed
  result := IntToHex(Seed, 8); 
  // then is followed by hexidecimal strings of each byte in the key
  for i := 0 to 3 do
    result := result + IntToHex(KeyBytes[i], 2);
  // add checksum to key string
  result := result + PKV_GetChecksum(result);
  // Add some hyphens to make it easier to type
  i := Length(result) - 3;
  while (i > 1) do
  begin
    Insert('-', result, i);
    Dec(i, 4);
  end;
end;

Important: never compile this valid key generator function into your release application! It is only to be used on your end to generate valid keys. The success of a PKVS is based on keeping the parameters used in the PKV_GetKeyByte call secret and away from the prying eyes of crackers.  Remember: if it’s in your compiled executable, a cracker can see it!

6. Testing a key in your application

Your application needs two functions for testing a key.

The first is a function for testing only the checksum value.  You’ll use this to test the key when a user types it in.  To make it harder for a cracker, this is all you want to test at first. More on this later.

The second function actually verifies portions of the key to see if they are valid, and also checks against the blacklist to see if a key should be rejected based on its seed value.

First we need to define the constants:

const
  KEY_GOOD = 0;
  KEY_INVALID = 1;
  KEY_BLACKLISTED = 2;
  KEY_PHONY = 3; 
  BL : array[0..0] of String = (
                                '11111111'
                               );

Above, BL is an array of blacklist strings. Important: only include the seed portion. Remember: if you put it in your program, a cracker can see it.  So do not put an entire key in the blacklist. That just makes it easier for a cracker to see what a valid key should look like.

Here is the checksum check function:

function PKV_CheckKeyChecksum(const Key : String) : Boolean;
var
  s, c : String;
begin
  result := False;
  // remove cosmetic hypens and normalize case
  s := UpperCase(StringReplace(Key, '-', '', [rfReplaceAll]));
  if Length(s) <> 20 then
    exit; // Our keys are always 20 characters long
  // last four characters are the checksum
  c := Copy(s, 17, 4);
  SetLength(s, 16);
  // compare the supplied checksum against the real checksum for
  // the key string.
  result := c = PKV_GetChecksum(s);
end;

And finally, we come to the function that tests keys for validity. In the sample code, I am using conditional defines to allow me to easily exclude “key bytes” from the checking function, but you could just as easily comment them out.  My advice is to only include one or two checks in a release, and to change which ones are checked for each release. Again, our example only has four “check bytes” but you should use many more.

function PKV_CheckKey(const S : String) : Integer;
var
  Key, kb : String;
  Seed : Int64;
  i : Integer;
  b : Byte;
begin
  result := KEY_INVALID;
  if not PKV_CheckKeyChecksum(S) then
    exit; // bad checksum or wrong number of characters
  // remove cosmetic hypens and normalize case
  Key := UpperCase(StringReplace(S, '-', '', [rfReplaceAll]));
  // test against blacklist
  if Length(BL) > 0 then
    for i := Low(BL) to High(BL) do
      if StartsStr(BL[i], Key) then
      begin
        result := KEY_BLACKLISTED;
        exit;
      end;
  // At this point, the key is either valid or forged,
  // because a forged key can have a valid checksum.
  // We now test the "bytes" of the key to determine if it is
  // actually valid.
  // When building your release application, use conditional defines
  // or comment out most of the byte checks!  This is the heart
  // of the partial key verification system. By not compiling in
  // each check, there is no way for someone to build a keygen that
  // will produce valid keys.  If an invalid keygen is released, you
  // simply change which byte checks are compiled in, and any serial
  // number built with the fake keygen no longer works.
  // Note that the parameters used for PKV_GetKeyByte calls MUST
  // MATCH the values that PKV_MakeKey uses to make the key in the
  // first place!
  result := KEY_PHONY;
  // extract the Seed from the supplied key string
  if not TryStrToInt64('$' + Copy(Key, 1, 8), Seed) then
    exit; 
  {$IFDEF KEY00}
  kb := Copy(Key, 9, 2);
  b := PKV_GetKeyByte(Seed, 24, 3, 200);
  if kb <> IntToHex(b, 2) then
    exit;
  {$ENDIF}
  {$IFDEF KEY01}
  kb := Copy(Key, 11, 2);
  b := PKV_GetKeyByte(Seed, 10, 0, 56);
  if kb <> IntToHex(b, 2) then
    exit;
  {$ENDIF}
  {$IFDEF KEY02}
  kb := Copy(Key, 13, 2);
  b := PKV_GetKeyByte(Seed, 1, 2, 91);
  if kb <> IntToHex(b, 2) then
    exit;
  {$ENDIF} 
  {$IFDEF KEY03}
  kb := Copy(Key, 15, 2);
  b := PKV_GetKeyByte(Seed, 7, 1, 100);
  if kb <> IntToHex(b, 2) then
    exit;
  {$ENDIF} 
  // If we get this far, then it means the key is either good, or was made
  // with a keygen derived from "this" release.
  result := KEY_GOOD;
end;

6. Making it harder for crackers

So far you have the tools you need to make a license key system that is virtually impervious to being “keygenned,” as far as valid keys go.  It is still possible for a cracker to alter your executable to skip key verification, and a cracker will still be able to create a keygen that works for whatever version of your application he has.  So what else is there to do?

  1. The first step I suggest is inlining the PKV_GetKeyByte, PKV_GetChecksum, PKV_CheckKeyChecksum, and PKV_CheckKey functions. Recent versions of Delphi support the inline compiler directive that forces the compiler to “unroll” the function in-place rather than actually make a function call. This results in larger code, but also gives the cracker that many more places to examine while he is dissecting your executable. It also prevents him from finding the single entry point for PKV_CheckKey and making it always return KEY_GOOD.
  2. When your application tests the stored key at startup, and when the user enters the key, only check the checksum. If your program immediately goes into its “key byte” verification code when it starts up or when the key is entered by the user, it’s just asking the cracker to watch carefully and see how it’s done!Just verify the checksum first, and give a polite error message if it is invalid (remember, this is where your customer is putting in his key that he gave you money for!). Elsewhere in your code, sprinkle the real checks into various operations.  User clicks File>Save? There’s a good time to really check the key, instead of just the checksum. Perhaps set up a timer and check it a full minute after the code is entered.  The possibilities are endless, and any unique ideas you come up with will make your program that much more tedious for a cracker to work on.  Crackers have lots of programs to fool around with, and if yours gets to be too frustrating, they may get sloppy and release only a partially working crack, or skip your application altogether.
  3. Keep on top of cracks, phony keygens, and leaked keys. Even though it isn’t possible for a cracker to create a fully valid key based on what you compiled into your program, he can (and probably will) release a keygen that works with whatever version he downloaded.  Set up a Google Alert for “Your_program keygen serial crack” and when one is released, immediately change which “key bytes” your program checks, or add the leaked key to the blacklist, and recompile. Suddenly none of the keys, keygens, and cracks released work anymore. Also, do a “silent” update of your download file when you are just recompiling for this reason. No need to announce to the cracker that their keygen suddenly doesn’t work any more.

7. Further development

This PKVS example is very simple in its implementation. There are several things that could be added to make it more powerful.

  1. Replace PKV_GetKeyByte with a more robust version. There are many hashing algorithms available that can be used as a starting point for calculating the portions of your key used to determine if it is valid.  Using the sample function presented here would be a mistake as it could be reverse engineered with valid leaked keys and brute-force algorithms.
  2. Instead of using hexidecimal, use the entire alphabet. This will let you pack a lot more data into the key string without making it too long. Note that the comparisons and byte generation checks in this example will have to be re-written to work with any other base numbering system.
  3. The example implementation is a simple “serial number” system and does not tie the key to a user name or other information. However, it would be trivial to make the seed itself a checksum value of another string, such as the user’s name.
  4. Anything could be added to the key string. You could add additional values to store activation or expiration dates, for example.
  5. Your key generation system should maintain a database of issued keys. You should always test your key check function against every key you have issued.  Also, keep a database of blacklisted or leaked keys, and always test your check function against them to ensure it returns the expected results.

Concluding remarks

Whatever you do, don’t focus all of your energy on your licensing system.  It is important, but creating usable software that customers need is more important. A good license key system will certainly improve your sales, but you can’t hope to convert every one searching for “MyProgram Serialz” into a customer.  A system like the one described here can be implemented in a day or two, but constantly trying to outsmart crackers is a never-ending battle.

27 Comments

  • Invar Brass says:

    Wow! A wonderful article! Thank you for the source code and the explanation! Keep up the good work.

  • Koms Bomb says:

    Great article, thank you, Brandon.
    Another simple way to anti-keygen entirely is using RSA to encrypt your key and never leak out the private key (that you use to encrypt).
    However, the only drawback is the key will become very long (128 bytes in binary, 170+ in base64).

  • Brandon says:

    Kombs Bomb,

    Yeah, you can’t exactly put a 170+ character serial number in a CD case and expect someone to type it in. :-)

  • Stuart Kelly says:

    Great article, thanks.

  • Kaushalya Damitha says:

    Great article, thanks.

  • Bob Boice says:

    Great article, Brandon! I appreciate you sharing it with me.

    Thank you!

  • Mike Wilson says:

    Even after all this time, your article is still the definitive PKV system explanation on the internet.

    Thanks Mr. Staggs!

    Best Regards,

    Mike

  • mora says:

    thanks for u’r code Mr. Staggs!

  • [...] Have a look at this article by Brandon Staggs on Partial Key Verification System in Delphi. http://www.brandonstaggs.com/2007/07…tem-in-delphi/ [...]

  • Stuart Kelly says:

    I referenced to your article on Stack Overflow. Here is a link to the question:

    http://stackoverflow.com/questions/715251/out-of-curiosity-how-are-serial-numbers-generated-hints-algorithms

  • Brandon says:

    Thanks, Stuart. I noticed that someone pointed out that these keys aren’t actually “serial numbers.” How pedantic of them. :-)

  • Michael says:

    Great article. :) How can you create a int64 (random seed) from a string? Which hash method suits best?

  • Damitha says:

    Thanks. this helped me lot. keep up the good work

  • Joe White says:

    FYI, unless something has changed recently, some of your statements about “inline” are not correct.

    The “inline” directive will not *force* the compiler to inline your code, only tell it that it’s *allowed* to inline it. In the words of Danny Thorpe (the guy who added the “inline” feature to the Delphi compiler), the compiler “can disregard it if it thinks you’re wrong or stupid”. (One example he gave was if inlining made your call site so complicated that you needed too many temps and blew your registers. In that case, the compiler might choose to make the call instead of inlining.)

    And the entry point might still exist. If every call is inlined, then the smart linker will remove the standalone function; but if any of the calls aren’t inlined, then the entry point will still exist. Granted, “inline” could make the code more resistant to cracking — even if the entry point still exists, some of the callers might use the inlined version instead. But if the check method is complicated, it probably gets more likely that the compiler will call the central function instead of inlining. (You can always look at the x86 code in the debug view to see whether it got inlined or not.)

    There’s more in-depth details about “inline” in the “Function inlining” section on this blog post (my notes from Danny’s BorCon presentation, clear back when the feature was introduced): http://blog.excastle.com/2004/09/13/dannys-whats-new-in-the-diamondback-compiler-session/

  • Ramshankar says:

    Good article, bookmarked for reading again.

  • Jamie Briant says:

    Seriously? You don’t appreciate dialing in to activate? Get over it! Your customers have. They do it for Windows, iTunes, iPhones, you name it. The only ones who don’t are huge companies like Ford, and you’ll be using FlexLM for them.

    Dialing in is 100% immune to keygens, so pirates will have to create a patch tool, which any sane individual will refuse to download and run. Nothing you can do to stop patch tools: the more you try, the more of a challenge it is for them – frankly some kid will do it just to prove he’s better than you.

  • Sam says:

    Hi, just visiting from Hacker News. Just curious, is there a high incidence of piracy among Bible software consumers?

  • Alvaro says:

    I don’t see the point, if you want security why don’t use a digital signature method?. With RSA, digital signatures are only 64 bits (16 hexa characters). And beign RSA an asymmetric method it is very difficult for hackers to generate valid serials without the private key.

  • KarlW says:

    Awesome article, thanks! Very simply explained for such a potentially complex procedure.

    Just a quick query… you state that the seed is 64-bit integer, however it is stored as 8 hex digits, which I thought could only represent a 32-bit number. Have I confused myself here? Or is this a mistake in the article?

    Thanks again,
    Karl :)

  • Vitor Rubio says:

    Great article. Thanks :)

  • K. Bolino says:

    @Jamie Briant

    No solution is “100% immune” to cracking. Even phoning-home/dialing-in can be spoofed, as a clever user can do the following:

    1. Packet-sniff a legitimate handshake with the real authentication server.
    2. Reverse-engineer the authentication protocol.
    3. Develop a custom server that emulates the protocol.
    4. Reconfigure the network (DNS and routing) to redirect authentication requests to the emulated server.
    5. Run the program as-is (i.e., no patches) in said environment until it receives the response it expects.

    You can introduce some cryptography to this process, but it will only frustrate an attacker, not completely defeat him. Of course, patching–which, “sane” or not, is commonplace–completely undermines key checking, with or without phoning home.

  • Olav says:

    Great article!

    I am thinking of implementing something like this into my own software.

    The thing I have issues with though, is making the operations secure enough.

    Another concern is the amount of data needed to implement for example 16 bit ckeck words instead of bytes. And having 6 or 8 instead of just 4. These could also be CRC16 values of some data.

    That makes the key very long…

    Any thoughts on that?

  • GuB says:

    @Jamie Briant
    No, dialing is annoying unless there is a legitimate (i.e. not related to copy protection) reason for it.
    iTunes legitimate reason is that it needs to connect to apple servers just to get contents. Windows needs to phone home to get updates. And BTW, Windows is a bad example, because it is usually the first thing people crack.

    The problem is that, by dialing home for no reason other that activation, you add a requirement that the cracked version won’t have. It makes the official version inferior to a well cracked version.

  • Henry says:

    Seed is a 64-bit integer. However, the seed value in your example has only 8 hex digits. How do you put a 64-bit integer into 8 hex digits?

  • Really well done article, thank you! It gives me enough inputs to start really considering serial protection implementation.

  • Olli says:

    Very good article!
    You are saying that the sample system uses a 64-bit seed value, but in the example key the seed is only 32 bit long A2’79’17’17.

  • Steve Sneed says:

    Brandon – this is still a great article; thanks much for keeping it up!

    @Jamie Briant – In addition to the other complaints against calling home: some systems have no external connectivity to the internet, such as industrial systems that for security reasons are locked behind a closed firewall (for example, systems used in power generation plants that must reside inside a NERC perimeter) so they have no way to call home. Yeah, it’s a specialized application, but specialized-market apps deserve security too.

 





Please note: Comment moderation is enabled and may delay your comment. There is no need to resubmit your comment.