Many types of software use Product Keys in order to prevent casual piracy. This is a fairly straightforward method of at least curbing it. It doesn’t put up a giant roadblock, but for corporate environments, working around it is a “smoking gun” for an audit since you don’t accidentally use a keygen. This makes it a fairly attractive option.
Another interesting way of making use of product key as a data storage mechanism, by encoding product capabilities and characteristics into the product key, you can have your program check that the capability is available before allowing the user to use it.
The most straightforward approach is quite simple in concept- you write some data to an encrypted stream, then take the bytes from that stream, base32 encode them stick some dashes on, and bobs your uncle and you’ve got a product key; determining if the key is valid, one merely reverses the process.
This is ripe for making more abstract; different applications could easily have different bits of information that could be encoded, and it should be fairly straightforward to “genericize” it such that the particular bits of key information can be persisted to and from a product key appropriately. We start with the abstract class for defining classes which contain product information to be encoded:
1 2 3 4 5 6 7 8 9 10 |
public abstract class LicenseKeyData { protected LicenseKeyData(Stream SourceData) { } protected LicenseKeyData() { } public abstract void PersistData(Stream Target); } |
No fancy fencing here; we define an abstract PersistData() method as well as a constructor. Of course, constructors cannot be defined abstract as they aren’t considered part of the interface, but we can enforce that at run-time. This is pretty much a Binary Formatter style interface; it writes to a Stream, and it reads from a Stream, and that’s it. The rest of the work is done elsewhere to encrypt and decrypt the data that was written to that stream into a Product Key. Of course you cannot write many Kilobytes of data to the stream and expect it to encode into a few bytes, so there is definitely a practical limit depending on how long you are willing to make the product keys themselves.
For the encryption, we can use something unique to the system as the password. As it happens, The cryptography registry information includes a unique Machine ID that we can use for this purpose:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
public static String LocalMachineID { get { String ReadKey = "SOFTWARE\\Microsoft\\Cryptography"; int keyresult = 0; RegistryKey rk = null; String mkid = ""; try { rk = Registry.LocalMachine.OpenSubKey(ReadKey); } catch { } if (rk != null) mkid = (String)rk.GetValue("MachineGuid"); if (String.IsNullOrEmpty(mkid)) { mkid = System.Environment.MachineName; } return mkid; } } |
I’ve found that the key isn’t always present. In particular, on some Windows XP systems it can be absent. For compatibility there is an extra consideration for that possibility, which will instead use the Machine Name. That is less unique and can be changed by the user very easily but it’s the next best thing. Also, I’m not trying to come up with the next SecuROM or something, so the added “Crackability” isn’t an issue. Furthermore, we can add a Salt to the encryption; for another piece of unique information, we can use the Windows installation product ID:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/// <summary> /// Returns the Windows Product ID of this system. /// </summary> public static String WindowsProductID { get { //HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\ CurrentVersion\ProductId String ReadKey = @"SOFTWARE\Microsoft\Windows NT\CurrentVersion"; int keyresult = 0; RegistryKey rk = null; String mkid = ""; try { rk = Registry.LocalMachine.OpenSubKey(ReadKey); } catch { } if (rk != null) mkid = (String)rk.GetValue("ProductId"); if (String.IsNullOrEmpty(mkid)) { mkid = System.Environment.MachineName; } return mkid; } } |
This product ID can be converted into a set of bytes by stripping out the dashes, and interpreting it as Hexadecimal. We can use this when constructing the Rfc2898DeriveBytes instance to use for encryption (or decryption) appropriately.
1 2 3 4 5 6 7 8 9 10 11 |
public static Rfc2898DeriveBytes GetPdb(string password) { String sWindowsID = WindowsProductID.Replace("-", ""); byte[] useSalt = Enumerable.Range(0, sWindowsID.Length) .Where(x => x%2 == 0) .Select(x => Convert.ToByte(sWindowsID.Substring(x, 2), 16)) .ToArray(); return new Rfc2898DeriveBytes (password, useSalt); } |
This gets used in what is otherwise a straightforward routine for encrypting and decrypting a byte array. The end result is we get fairly straightforward use. First, we define a class that derives from LicenseKeyData, which saves and restores some bits of data to and from the stream via the constructor and the abstract method implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
public class StandardKey : LicenseKeyData { public enum Edition { Standard, Professional, Enterprise, Ultimate } public Edition LicensedEdition { get; set; } public byte LicensedMajorVersion { get; set; } public byte LicensedMinorVersion { get; set; } public UInt16 LicensedUsers { get; set; } public DateTime ExpiryDate { get; set; } public StandardKey() { } private byte calcheader() { return (byte)(((LicensedUsers + (Int16)((Math.Pow((double)LicensedEdition, (double)ExpiryDate.Day)))) + (Math.Pow(LicensedMajorVersion, ((DateOnly)ExpiryDate).ToInt()))) % 255); } public override string ToString() { return LicensedEdition.ToString() + " Edition, Major version " + LicensedMajorVersion + " Minor Version:" + LicensedMinorVersion + " Licensed for " + LicensedUsers + " Users, Expires " + ExpiryDate.ToString(); } /// <summary> /// calculates our footer value for the given header value. /// </summary> /// <param name="headervalue"></param> /// <returns>/returns> private byte calcfooter(byte headervalue) { return (byte) ((((DateOnly)ExpiryDate).ToInt() - (LicensedMajorVersion * LicensedMinorVersion) / (byte)(LicensedEdition + 1) + LicensedUsers ^ headervalue) % 255); } public StandardKey(Stream SourceData) : base(SourceData) { BinaryReader br = new BinaryReader(SourceData); byte Major, Minor, Revision; UInt16 Build; byte header = br.ReadByte(); LicensedEdition = (Edition)br.ReadInt16(); LicensedMajorVersion = br.ReadByte(); LicensedMinorVersion = br.ReadByte(); LicensedUsers = br.ReadUInt16(); byte exDay, exMonth; UInt16 exYear; exDay = (byte)br.ReadByte(); exMonth = (byte)br.ReadByte(); exYear = br.ReadUInt16(); ExpiryDate = new DateOnly(exDay, exMonth, exYear); byte footer = br.ReadByte(); byte calculatedHeader, calculatedFooter; calculatedHeader = calcheader(); calculatedFooter = calcfooter(header); if (calculatedHeader != header || calculatedFooter != footer) { throw new InvalidKeyException("Invalid Product Key."); } } public override void PersistData(Stream Target) { //add header/footer ints with verification of validity... Byte Major, Minor, Revision; UInt16 Build; byte Header = calcheader(); byte Footer = calcfooter(Header); Major = LicensedMajorVersion; Minor = LicensedMinorVersion; BinaryWriter bw = new BinaryWriter(Target); bw.Write(Header); bw.Write((short)LicensedEdition); bw.Write(Major); bw.Write(Minor); bw.Write(LicensedUsers); DateOnly dw = new DateOnly(ExpiryDate); bw.Write(dw.Day); bw.Write(dw.Month); bw.Write(dw.Year); bw.Write(Footer); } } |
The Handler class uses type constraints to accept only LicenseKeyData derived classes as part of two generic methods to effectively retrieve the data from a product key or encode it into a product key. This makes for relatively straightforward usage:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
static void Main(string[] args) { StandardKey lk = new StandardKey(); lk.ExpiryDate = DateTime.Now.AddMonths(1); lk.LicensedEdition = StandardKey.Edition.Professional; lk.LicensedUsers = 50; lk.LicensedMajorVersion = 8; lk.LicensedMinorVersion = 2; String sKey = CryptHelper.InsertDashes(LicenseHandler.ToProductCode<StandardKey>(lk, CryptHelper.LocalMachineID)); Console.WriteLine(lk); Console.WriteLine("Generated Product Key:" + sKey); Console.WriteLine("Decrypting Product Key..."); StandardKey decryptKey = LicenseHandler.FromProductCode<StandardKey>(sKey, CryptHelper.LocalMachineID); Console.WriteLine("Decrypted Result:" + decryptKey.ToString()); sKey = "5" + sKey.Substring(1); Console.WriteLine("Changed one character in key- using " + sKey); try { StandardKey faildecrypt = LicenseHandler.FromProductCode<StandardKey>(sKey, CryptHelper.LocalMachineID); } catch (Exception exx) { Console.WriteLine("Exception:" + exx.Message); } Console.ReadKey(); } |
The guts of the LicenseHandler and implementation of the ToProductCode() and FromProductCode() methods are interesting as well. The ToProductCode<T> definition accepts the derived class instance, and calls the interface method to write to it. Then it converts the data in the MemoryStream to a byte array, encrypts it, base32 encodes it and adds dashes, then returns that as the product Key:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public static String ToProductCode<T>(T LicenseObject, String IDString) where T : LicenseKeyData { MemoryStream mstream = new MemoryStream(); //first, write our data out to a memorystream. LicenseObject.PersistData(mstream); //seek to the start, read as a string. mstream.Seek(0, SeekOrigin.Begin); //read it back, as a array of bytes. Byte[] readdata = new byte[mstream.Length]; mstream.Read(readdata, 0, readdata.Length); Byte[] encrypted = CryptHelper.Encrypt(readdata, IDString); //now we need a readable form, so encode using zBase32, which has //good results for a human readable key. ZBase32Encoder zb = new ZBase32Encoder(); return zb.Encode(encrypted).ToUpper(); } |
So now we can generate product keys. Of course, we need to be able to read the information back. As one might expect, we effectively reverse the process with the FromProductCode<T> implementation:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static T FromProductCode<t>(String pProductCode, String IDString) where T : LicenseKeyData { // remove any hyphens/ dashes, first. pProductCode = pProductCode.Replace("-", ""); //convert the string to a byte array via zBase32: ZBase32Encoder zb = new ZBase32Encoder(); byte[] acquiredcode = zb.Decode(pProductCode); //now, we need to decrypt it... byte[] decrypted = CryptHelper.Decrypt(acquiredcode, IDString); //armed with the decrypted data, toss it into a memory stream MemoryStream ms = new MemoryStream(decrypted); ms.Seek(0, SeekOrigin.Begin); //now, invoke FromStream... return FromDecryptedStream</t><t>(ms); }</t> |
Hey, wait a second- That’s cheating, isn’t it? It all ends with FromDecryptedStream<T>, which means we’re missing some of the process- arguably, the most interesting part! Here’s that method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public static T FromDecryptedStream<T>(Stream Source) where T : LicenseKeyData { if (!Source.CanRead) throw new ArgumentException("Stream must be readable.", "readFrom"); try { ConstructorInfo useConstructor = typeof (T).GetConstructor(new Type[] {typeof (Stream)}); if (useConstructor == null) { throw new ArgumentException("Type " + typeof (T).Name + " does not have a constructor accepting arguments of type (Stream)"); } Object BuiltInstance = useConstructor.Invoke(new object[] {Source}); return (T) BuiltInstance; } catch (Exception Exx) { throw new InvalidKeyException("Invalid Product Key", Exx); } } |
This part does the interesting part- It accepts the “standard” stream after we’ve decrypted the information and it’s been tossed into a Memory Stream; we simply find the appropriate constructor and toss the Stream at it and return the resulting instance. We of course throw an appropriate exception if the constructor isn’t found, and if an exception occurs, we will presume that the key is invalid and throw an exception appropriately.
It is, of course, possible to go further. In particular, one could consider the case of a product that is licensed for a specific number of users. One approach for this would be to have a Server program that is given the product key appropriately; the programs on each station will be a stub program with an encrypted block at the end; the stub program connects to the license server and requests the decryption key- if there are fewer than the licensed number of users using the software package, it will increment the count it has internally, and provide the decryption key. The stub program decrypts the data block using the provided key, saves that decrypted data as an executable, and runs it. There would likely be some task in managing the concurrent users, in particular, a user of any product in a suite might be considered only one concurrent user, regardless of the number of programs they are actually running concurrently. But the approach of actually encrypting the data and requiring a key from a license server via a stub program can help prevent users from working around otherwise basic preventions that just skip portions of logic.
The code for this project can be found On Github
Have something to say about this post? Comment!