Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Check if a generated license is valid

I have a PHP script that generates some strings which will be used as license keys:

function KeyGen(){
     $key = md5(microtime());
     $new_key = '';
     for($i=1; $i <= 25; $i ++ ){
               $new_key .= $key[$i];
               if ( $i%5==0 && $i != 25) $new_key.='-';
     }
  return strtoupper($new_key);
  }
$x = 0;
while($x <= 10) {
  echo KeyGen();
  echo "<br />";
$x++; 
}

After running the script once, I got these:

8B041-EC7D2-0B9E3-09846-E8C71
C8D82-514B9-068BC-8BF80-05061
A18A3-E05E5-7DED7-D09ED-298C4
FB1EC-C9844-B9B20-ADE2F-0858F
E9AED-945C8-4BAAA-6938D-713ED
4D284-C5A3B-734DF-09BD6-6A34C
EF534-3BAE4-860B5-D3260-1CEF8
D84DB-B8C72-5BDEE-1B4FE-24E90
93AF2-80813-CD66E-E7A5E-BF0AE
C3397-93AA3-6239C-28D9F-7A582
D83B8-697C6-58CD1-56F1F-58180

What I now am trying to do is change it so that I have another function that will check if the key has been generated using my script. Currently, what I am thinking is setting the $key to the MD5 of one specific string (for example, test) but, of course, that returns all the strings the same.

Can anyone help?

like image 253
Benedict Lewis Avatar asked Apr 25 '13 11:04

Benedict Lewis


3 Answers

There are three basic ways of handling this. How you do it will depend on how many keys you're generating, and how important is may be to be able to invalidate keys at a later day. Which you choose is up to you.

Option 1: Server Database Storage

When the server generates a key (like using your algorithm), you store it in a database. Then later all you need to do to check the key is see if it's in the database.

Note that your algorithm needs a lot more entropy than you're providing it. The current timestamp is NOT enough. Instead, use strong randomness:

$key = mcrypt_create_iv($length_needed, MCRYPT_DEV_URANDOM);

Or, if you don't have mcrypt:

$key = openssl_random_pseudo_bytes($length_needed);

Or if you don't have mcrypt and openssl, use a library

Note that md5 returns a hex output (a-f0-9), where all of the above return full random binary strings (characters 0 - 255). So either base64_encode() it, or bin2hex() it.

Pros:

  • Simple to implement
  • Can "deactive" issued keys at a later date
  • Impossible to forge a new key

Cons:

  • Requires persistent storage per key
  • May not scale that well
  • Requires "key server" to validate keys

Option 2: Signing Keys

Basically, you generate a strong random key (from here out called the private key), and store it on your server. Then, when generating the license key, you generate a random blob, and then HMAC sign it with the private key, and make the license part of that block. That way, you don't need to store each individual key.

function create_key($private_key) {
    $rand = mcrypt_create_iv(10, MCRYPT_DEV_URANDOM);
    $signature = substr(hash_hmac('sha256', $rand, $private_key, true), 0, 10);
    $license = base64_encode($rand . $signature);
    return $license;
}

function check_key($license, $private_key) {
    $tmp = base64_decode($license);
    $rand = substr($tmp, 0, 10);
    $signature = substr($tmp, 10);
    $test = substr(hash_hmac('sha256', $rand, $private_key, true), 0, 10);
    return $test === $signature;
}
    

Pros:

  • Simple to implement
  • Does not require persistent storage
  • Trivial to scale

Cons:

  • Cannot "Deactivate" keys individual
  • Requires storing "private keys"
  • Requires "key server" to validate keys.

Option 3: Public Key Crypto

Basically, you generate a public/private key pair. You embed the public key in your application. Then, you generate a key (similar to "signing keys" above), but instead of signing it with the HMAC signature, you sign it with a private key.

That way, the application (which has the public key) can verify the signature directly without needing to call back to your server.

function create_key($private_key) {
    $rand = mcrypt_create_iv(10, MCRYPT_DEV_URANDOM);

    $pkeyid = openssl_get_privatekey($private_key);
    openssl_sign($rand, $signature, $pkeyid);
    openssl_free_key($pkeyid);

    $license = base64_encode($rand . $signature);
    return $license;
}

function check_key($license, $public_key) {
    $tmp = base64_decode($license);
    $rand = substr($tmp, 0, 10);
    $signature = substr($tmp, 10);

    $pubkeyid = openssl_get_publickey($public_key);
    $ok = openssl_verify($rand, $signature, $pubkeyid);
    openssl_free_key($pubkeyid);

    return $ok === 1;
}

Pros:

  • Simple to implement
  • Does not require persistent storage
  • Trivial to scale
  • Does not require "key server" to validate keys

Cons:

  • Cannot "Deactivate" keys individual
  • Requires storing "private keys"
like image 101
ircmaxell Avatar answered Nov 15 '22 17:11

ircmaxell


Note:

This solution is on the assumption you want your licence key to always be in fixed format (see below) and still self authenticated

  FORMAT : XXXXX-XXXXX-XXXXX-XXXXX-XXXX

If that is not the case refer to @ircmaxell for a better solution

Introduction

Self authenticated serial is tricky solution because:

  • Limited Size of Serial
  • It need to authenticate it self without Database or any storage
  • If private key is leaked .. it can easily be reversed

Example

$option = new CheckProfile();
$option->name = "My Application"; // Application Name
$option->version = 0.9; // Application Version
$option->username = "Benedict Lewis"; // you can limit the key to per user
$option->uniqid = null; // add if any


$checksum = new Checksum($option);
$key = $checksum->generate();
var_dump($key, $checksum->check($key));

Output

string '40B93-C7FD6-AB5E6-364E2-3B96F' (length=29)
boolean true

Please note that any modification in the Options would change the key and make it invalid;

Checking for collision

I just ran this simple test

set_time_limit(0);

$checksum = new Checksum($option);
$cache = array();
$collision = $error = 0;
for($i = 0; $i < 100000; $i ++) {
    $key = $checksum->generate();
    isset($cache[$key]) and $collision ++;
    $checksum->check($key) or $error ++;
    $cache[$key] = true;
}

printf("Fond %d collision , %d Errors  in 100000 entries", $collision, $error);

Output

  Fond 0 collision , 0 Errors in 100000 entries 

Better Security

By default the script uses sha1 but PHP has a lot of better hash functions you can get that with the following code

print_r(hash_algos());

Example

$checksum = new Checksum($option, null, "sha512");

Class Used

class Checksum {
    // Used used binaray in Hex format
    private $privateKey = "ec340029d65c7125783d8a8b27b77c8a0fcdc6ff23cf04b576063fd9d1273257"; // default
    private $keySize = 32;
    private $profile;
    private $hash = "sha1";

    function __construct($option, $key = null, $hash = "sha1") {
        $this->profile = $option;
        $this->hash = $hash;

        // Use Default Binary Key or generate yours
        $this->privateKey = ($key === null) ? pack('H*', $this->privateKey) : $key;
        $this->keySize = strlen($this->privateKey);
    }

    private function randString($length) {
        $r = 0;
        switch (true) {
            case function_exists("openssl_random_pseudo_bytes") :
                $r = bin2hex(openssl_random_pseudo_bytes($length));
                break;
            case function_exists("mcrypt_create_ivc") :
            default :
                $r = bin2hex(mcrypt_create_iv($length, MCRYPT_DEV_URANDOM));
                break;
        }
        return strtoupper(substr($r, 0, $length));
    }

    public function generate($keys = false) {
        // 10 ramdom char
        $keys = $keys ?  : $this->randString(10);
        $keys = strrev($keys); // reverse string

        // Add keys to options
        $this->profile->keys = $keys;

        // Serialise to convert to string
        $data = json_encode($this->profile);

        // Simple Random Chr authentication
        $hash = hash_hmac($this->hash, $data, $this->privateKey);
        $hash = str_split($hash);

        $step = floor(count($hash) / 15);
        $i = 0;

        $key = array();
        foreach ( array_chunk(str_split($keys), 2) as $v ) {
            $i = $step + $i;
            $key[] = sprintf("%s%s%s%s%s", $hash[$i ++], $v[1], $hash[$i ++], $v[0], $hash[$i ++]);
            $i ++; // increment position
        }
        return strtoupper(implode("-", $key));
    }

    public function check($key) {
        $key = trim($key);
        if (strlen($key) != 29) {
            return false;
        }
        // Exatact ramdom keys
        $keys = implode(array_map(function ($v) {
            return $v[3] . $v[1];
        }, array_map("str_split", explode("-", $key))));

        $keys = strrev($keys); // very important
        return $key === $this->generate($keys);
    }
}
like image 14
12 revs Avatar answered Nov 15 '22 16:11

12 revs


What you are actually looking for is an algorithm like Partial Key Validation

See this article for the workings and port it to PHP

http://www.brandonstaggs.com/2007/07/26/implementing-a-partial-serial-number-verification-system-in-delphi/

like image 2
nvanesch Avatar answered Nov 15 '22 18:11

nvanesch