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();
}