8

I have devices with unique serial number (string incremetation) ex : AS1002 and AS1003.

I need to figure out an algorithm to produce a unique activation key for each serial number.

What would be the best approach for this ?

Thanks !

(This has to be done offline)

Ta01
  • 31,040
  • 13
  • 70
  • 99
David
  • 429
  • 1
  • 4
  • 10
  • Some good thoughts here: http://stackoverflow.com/questions/453030/how-can-i-create-a-product-key-for-my-c-app – Ta01 Sep 22 '11 at 15:53
  • if it has to be done offline, it's never going to be secure anyway – harold Sep 22 '11 at 15:57

6 Answers6

2

Activation Key

Here is a simple structure of the activation key:

Part Description
Data A part of the key encrypted with a password. Contains the key expiration date and application options.
Hash Checksum of the key expiration date, password, options and environment parameters.
Tail The initialization vector that used to decode the data (so-called "salt").
class ActivationKey
{
    public byte[] Data { get; set; } // Encrypted part.
    public byte[] Hash { get; set; } // Hashed part.
    public byte[] Tail { get; set; } // Initialization vector.
}

The key could represent as text format: DATA-HASH-TAIL. For example:
KCATBZ14Y-VGDM2ZQ-ATSVYMI.
The folowing tool will use cryptographic transformations to generate and verify the key.

Generating

The algorithm for obtaining a unique activation key for a data set consists of several steps:

  • data collection,
  • getting the hash and data encryption,
  • converting activation key to string.

Data collection

At this step, you need to get an array of data such as serial number, device ID, expiration date, etc. This purpose can be achieved using the following

method:

unsafe byte[] Serialize(params object[] objects)
{
  using (MemoryStream memory = new MemoryStream())
  using (BinaryWriter writer = new BinaryWriter(memory))
  {
    foreach (object obj in objects)
    {
      if (obj == null) continue;
      switch (obj)
      {
        case string str:
          if (str.Length > 0)
            writer.Write(str.ToCharArray());
          continue;
        case DateTime date:
          writer.Write(date.Ticks);
          continue;
        case bool @bool:
          writer.Write(@bool);
          continue;
        case short @short:
          writer.Write(@short);
          continue;
        case ushort @ushort:
          writer.Write(@ushort);
          continue;
        case int @int:
          writer.Write(@int);
          continue;
        case uint @uint:
          writer.Write(@uint);
          continue;
        case long @long:
          writer.Write(@long);
          continue;
        case ulong @ulong:
          writer.Write(@ulong);
          continue;
        case float @float:
          writer.Write(@float);
          continue;
        case double @double:
          writer.Write(@double);
          continue;
        case decimal @decimal:
          writer.Write(@decimal);
          continue;
        case byte[] buffer:
          if (buffer.Length > 0)
            writer.Write(buffer);
          continue;
        case Array array:
          if (array.Length > 0)
            foreach (var a in array) writer.Write(Serialize(a));
          continue;
        case IConvertible conv:
          writer.Write(conv.ToString(CultureInfo.InvariantCulture));
          continue;
        case IFormattable frm:
          writer.Write(frm.ToString(null, CultureInfo.InvariantCulture));
          continue;
        case Stream stream:
          stream.CopyTo(stream);
          continue;
        default:
          try
          {
            int rawsize = Marshal.SizeOf(obj);
            byte[] rawdata = new byte[rawsize];
            GCHandle handle = GCHandle.Alloc(rawdata, GCHandleType.Pinned);
            Marshal.StructureToPtr(obj, handle.AddrOfPinnedObject(), false);
            writer.Write(rawdata);
            handle.Free();
          }
          catch(Exception e)
          {
            // Place debugging tools here.
          }
          continue;
      }
    }
    writer.Flush();
    byte[] bytes = memory.ToArray();
    return bytes;
  }
}

Getting the hash and data encryption

This step contains the following substeps:

  • create an encryption engine using a password and stores the initialization vector in the Tail property.
  • next step, expiration date and options are encrypted and the encrypted data is saved into the Data property.
  • finally, the hashing engine calculates a hash based on the expiration date, password, options and environment and puts it in the Hash property.
ActivationKey Create<TAlg, THash>(DateTime expirationDate, 
                                  object password, 
                                  object options = null, 
                                  params object[] environment)
    where TAlg : SymmetricAlgorithm
    where THash : HashAlgorithm
{
    ActivationKey activationKey = new ActivationKey();
    using (SymmetricAlgorithm cryptoAlg = Activator.CreateInstance<TAlg>())
    {
        if (password == null)
        {
            password = new byte[0];
        }
        activationKey.Tail = cryptoAlg.IV;
        using (DeriveBytes deriveBytes = 
        new PasswordDeriveBytes(Serialize(password), activationKey.Tail))
        {
            cryptoAlg.Key = deriveBytes.GetBytes(cryptoAlg.KeySize / 8);
        }
        expirationDate = expirationDate.Date;
        long expirationDateStamp = expirationDate.ToBinary();
        using (ICryptoTransform transform = cryptoAlg.CreateEncryptor())
        {
            byte[] data = Serialize(expirationDateStamp, options);
            activationKey.Data = transform.TransformFinalBlock(data, 0, data.Length);
        }
        using (HashAlgorithm hashAlg = Activator.CreateInstance<THash>())
        {
            byte[] data = Serialize(expirationDateStamp, 
                                    cryptoAlg.Key, 
                                    options, 
                                    environment, 
                                    activationKey.Tail);
            activationKey.Hash = hashAlg.ComputeHash(data);
        }
    }
    return activationKey;
}

Converting to string

Use the ToString method to get a string containing the key text, ready to be transfering to the end user.

N-based encoding (where N is the base of the number system) was often used to convert binary data into a human-readable text. The most commonly used in activation key is base32. The advantage of this encoding is a large alphabet consisting of numbers and letters that case insensitive. The downside is that this encoding is not implemented in the .NET standard library and you should implement it yourself. You can also use the hex encoding and base64 built into mscorlib. In my example base32 is used, but I will not give its source code here. There are many examples of base32 implementation on this site.

string ToString(ActivationKey activationKey)
{
    if (activationKey.Data == null 
       || activationKey.Hash == null 
       || activationKey.Tail == null)
    {
        return string.Empty;
    }
    using (Base32 base32 = new Base32())
    {
        return base32.Encode(activationKey.Data) 
               + "-" + base32.Encode(activationKey.Hash) 
               + "-" + base32.Encode(activationKey.Tail);
    }
}

To restore use the folowing method:

ActivationKey Parse(string text)
{
    ActivationKey activationKey;
    string[] items = text.Split('-');
    if (items.Length >= 3)
    {
        using (Base32 base32 = new Base32())
        {
            activationKey.Data = base32.Decode(items[0]);
            activationKey.Hash = base32.Decode(items[1]);
            activationKey.Tail = base32.Decode(items[2]);
        }
    }
    return activationKey;
}

Checking

Key verification is carried out using methodes GetOptions an Verify.

  • GetOptions checks the key and restores embeded data as byte array or null if key is not valid.
  • Verify just checks the key.
byte[] GetOptions<TAlg, THash>(object password = null, params object[] environment)
    where TAlg : SymmetricAlgorithm
    where THash : HashAlgorithm
{
    if (Data == null || Hash == null || Tail == null)
    {
        return null;
    }
    try
    {
        using (SymmetricAlgorithm cryptoAlg = Activator.CreateInstance<TAlg>())
        {
            cryptoAlg.IV = Tail;
            using (DeriveBytes deriveBytes = 
            new PasswordDeriveBytes(Serialize(password), Tail))
            {
                cryptoAlg.Key = deriveBytes.GetBytes(cryptoAlg.KeySize / 8);
            }
            using (ICryptoTransform transform = cryptoAlg.CreateDecryptor())
            {
                byte[] data = transform.TransformFinalBlock(Data, 0, Data.Length);
                int optionsLength = data.Length - 8;
                if (optionsLength < 0)
                {
                    return null;
                }
                byte[] options;
                if (optionsLength > 0)
                {
                    options = new byte[optionsLength];
                    Buffer.BlockCopy(data, 8, options, 0, optionsLength);
                }
                else
                {
                    options = new byte[0];
                }
                long expirationDateStamp = BitConverter.ToInt64(data, 0);
                DateTime expirationDate = DateTime.FromBinary(expirationDateStamp);
                if (expirationDate < DateTime.Today)
                {
                    return null;
                }
                using (HashAlgorithm hashAlg = 
                Activator.CreateInstance<THash>())
                {
                    byte[] hash = 
                    hashAlg.ComputeHash(
                         Serialize(expirationDateStamp, 
                                   cryptoAlg.Key, 
                                   options, 
                                   environment, 
                                   Tail));
                    return ByteArrayEquals(Hash, hash) ? options : null;
                }
            }
        }
    }
    catch
    {
        return null;
    }
}

bool Verify<TAlg, THash>(object password = null, params object[] environment)
    where TAlg : SymmetricAlgorithm
    where THash : HashAlgorithm
{
    try
    {
        byte[] key = Serialize(password);
        return Verify<TAlg, THash>(key, environment);
    }
    catch
    {
        return false;
    }
}

Example

Here is a full example of generating the activation key using your own combination of any amount of data - text, strings, numbers, bytes, etc.

Example of usage:

string serialNumber = "0123456789"; // The serial number.
const string appName = "myAppName"; // The application name.

// Generating the key. All the parameters passed to the costructor can be omitted.
ActivationKey activationKey = new ActivationKey(
//expirationDate:
DateTime.Now.AddMonths(1),  // Expiration date 1 month later.
                            // Pass DateTime.Max for unlimited use.
//password:
null,                       // Password protection;
                            // this parameter can be null.
//options:
null                       // Pass here numbers, flags, text or other
                           // that you want to restore 
                           // or null if no necessary.
//environment:
appName, serialNumber      // Application name and serial number.
);
// Thus, a simple check of the key for validity is carried out.
bool checkKey = activationKey.Verify((byte[])null, appName, serialNumber);
if (!checkKey)
{
  MessageBox.Show("Your copy is not activated! Please get a valid activation key.");
  Application.Exit();
}
ng256
  • 84
  • 2
2

You have two things to consider here:
- Whatever key you generate must be able to be entered easily, so this eliminates some weird hash which may produce characters which will be cumbersome to type, although this can be overcome, it’s something you should consider.
- The operation as you stated must be done online

Firstly, there will be no way to say with absolute certainty that someone will not be able to decipher your key generation routine, no matter how much you attempt to obfuscate. Just do a search engine query for “Crack for Xyz software”.

This has been a long battle that will never end, hence the move to deliver software as services, i.e. online where the producer has more control over their content and can explicitly authorize and authenticate a user. In your case you want to do this offline. So in your scenario someone will attach your device to some system, and the accompanying software that you intend to write this routine on will make a check against the serial number of the device v/s user input.

Based on @sll’s answer, given the offline nature of your request. Your best, unfortunately would be to generate a set of random codes, and validate them when user’s call in.

Here is a method borrowed from another SO answer, I've added digits as well

private readonly Random _rng = new Random();
private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; //Added 1-9

private string RandomString(int size)
{
    char[] buffer = new char[size];

    for (int i = 0; i < size; i++)
    {
        buffer[i] = _chars[_rng.Next(_chars.Length)];
    }
    return new string(buffer);
}

So, generating one for each of your devices and storing them somewhere might be your only option because of the offline considerations.

This routine will produce strings like this when set to create a 10 digit string, which is reasonably random.

3477KXFBDQ
ROT6GRA39O
40HTLJPFCL
5M2F44M5CH
CAVAO780NR
8XBQ44WNUA
IA02WEWOCM
EG11L4OGFO
LP2UOGKKLA
H0JB0BA4NJ
KT8AN18KFA

Community
  • 1
  • 1
Ta01
  • 31,040
  • 13
  • 70
  • 99
1

If your device has some secured memory which can not be read by connecting an programmator or an other device -you can store some key-code and then use any hashing algorithm like MD5 or SHA-1/2 to generate hash by:

HASH(PUBLIC_SERIALNUMBER + PRIVATE_KEYCODE)

And pairs of SERIALNUMBER + KEYCODE should be stored in local DB.

In this way: (offline)

  • Client calling you and asking for the Activation Code
  • You asking for a SERIALNUMBER of particular device
  • Then you search for a KEYCODE by a given SERIALNUMBER in your local DB and generate Activation Code (even using MD5 this will be sacure as long KEYCODE is privately stored in your DB)
  • Client enter Activation Code into the device, device able to generate hash by own SERIALNUMBER and KEYCODE and then compare to Activation Code entered by user

This could be simplified by storing activation code itself if device has a secured memory onboard (like SmartCards has). In this way you can just keep own database of SerialCode - ActivationCode pairs.

sll
  • 61,540
  • 22
  • 104
  • 156
  • If you're going for security, then drop the fallacy that MD5 is secure. – Grant Thomas Sep 22 '11 at 15:41
  • @Mr. Disappointment : Have I said that MD5 is secure before? Basically I'm saying opposite, sorry if confused you :) – sll Sep 22 '11 at 15:44
  • Perhaps that is what you meant, but the following from your original input seems contradictory to that: '_for instance I'll use MD5 or SHA-1/2'_ – Grant Thomas Sep 22 '11 at 15:46
  • Yep but also I said that `anyone can generate activation key by serial number because hash would be the same.` what obviously mean that this is not secure way – sll Sep 22 '11 at 15:47
  • But that is irrelevant in this context, as that is mostly applicable to any hash algorithm: one input results in one, the same, output; this says nothing of security but is an inherent property of hashing. The _lack_ of security with MD5 and some other algorithms, is that the hashed value can be reverse-engineered to the original input (without knowing the original input.) – Grant Thomas Sep 22 '11 at 15:50
  • How would any of that matter for a simple activation process? You can't easily derive the serial number from the hash, and you can't get a hash without the serial number. Presumably the user knows the serial, so what's the risk here? – drharris Sep 22 '11 at 15:57
  • Because the serial _is_ the original input, which means the serial _can_ be derived from the MD5 hash. – Grant Thomas Sep 22 '11 at 17:00
  • That's not much of a security risk. The OP said each device was given a serial number with a simple increment, so that's not much of a secret. The secret here is the hash used, and any salt you append to it. Salted hashes are a decently secure way to do something like this, requiring some reverse engineering to figure out how to duplicate it. If the salt is unique and secret enough, then what is the problem? – drharris Sep 22 '11 at 17:11
  • I've proposed KEYCODE as salt to be used whilst hashing – sll Sep 22 '11 at 17:58
  • 1
    You're advocating security through obscurity. Do we really have to have the argument that that's a bad idea? – Nick Johnson Sep 23 '11 at 01:07
  • @Nick Johnson : what you mean regarding bad idea? Hashing? If so - In begionning of the my answer I mean that usign hashing based on SerialNumber only, but as final solution I proposet to hash SERIALNUMBER_KEYCODE, where is KEYCODE - private string/code. – sll Sep 23 '11 at 09:11
  • @sli "Basically user which has serial number would generate valid serial himself so avoid any public and well-known hash algorithms." – Nick Johnson Sep 25 '11 at 05:01
  • @Nick Johnson : yep agreed that it sounds incorrect in such generalized form, I must edit answer and just leave last sentences regarding hash algorithms (basically that it could be used with some kind of private part of a string which going to be hashed) – sll Sep 25 '11 at 19:16
1

By far the most secure way to do it is to have a centralized database of (serial number, activation key) pairs and have the user activate over the internet so you can check the key locally (on the server).

In this implementation, the activation key can be completely random since it doesn't need to depend on the serial number.

Blindy
  • 65,249
  • 10
  • 91
  • 131
1

You want it to be easy to check, and hard to "go backwards". You'll see a lot of suggestions for using hashing functions, those functions are easy to go one way, but hard to go backwards. Previously, I phrased that as "it is easy to turn a cow into a hamburger, but hard to turn a hamburger into a cow". In this case, a device should know its own serial number and be able to "add" (or append) some secret (usually called "salt") to the serial and then hash or encrypt it.

If you are using reversible encryption, you want to add some sort of "check digit" to the serial numbers so that if someone does figure your encryption scheme out, there is another layer for them to figure out.

An example of a function that is easy enough to "go backwards" was one I solved with Excel while trying to avoid homework.

And you probably want to make things easier for your customers by making the encoding less likely to be messed up when the activation codes are handwritten (such as you write it down from the email then walk over to where the device is and punch the letters/digits in). In many fonts, I and 1, and 0 and O are similar enough that many encodings, such as your car's VIN do not use the letters i and o (and I remember older typewriters that lacked a key for the digit 1 because you were expected to use lowercase L). In such cases, Y, 4 and 7 can appear the same depending on some handwriting. So know your audience and what are their limits.

Community
  • 1
  • 1
Tangurena
  • 2,121
  • 1
  • 22
  • 41
  • A salt is not secret and is used to prevent precomputation attacks. In this case, it's a key, and should be used in an HMAC. – Nick Johnson Sep 23 '11 at 01:05
0

How about: Invent a password that is not revealed to the user. Then concatenate this password with the serial number and hash the combination.

Anything you do can be broken by a dedicated enough hacker. The question is not, "Can I create absolutely unbreakable security?" but "Can I create security good enough to protect against unskilled hackers and to make it not worth the effort for the skilled hackers?" If you reasonably expect to sell 10 million copies of your product, you'll be a big target and there may be lots of hackers out there who will try to break it. If you expect to sell a few hundred or maybe a few thousand copies, not so much.

Jay
  • 26,876
  • 10
  • 61
  • 112