Playing with key generators, redux
In my previous post, I have discussed how using Smaz-like compression, I can achieve a smaller (and more professional looking) license key.
Here is the original license:
{ "id": "cd6fff02-2aff-4fae-bc76-8abf1e673d3b", "expiration": "2017-01-17T00:00:00.0000000", "type": "Subscription", "version": "3.0", "maxRamUtilization": "12884901888", "maxParallelism": "6", "allowWindowsClustering": "false", "OEM": "false", "numberOfDatabases": "unlimited", "fips": "false", "periodicBackup": "true", "quotas": "false", "authorization": "true", "documentExpiration": "true", "replication": "true", "versioning": "true", "maxSizeInMb": "unlimited", "ravenfs": "true", "encryption": "true", "compression": "false", "updatesExpiration": "2017-Jan-17", "name": "Hibernating Rhinos" }
And here is the resulting output:
0172020007 93050A0B28 0D0D0D682C 24080D0D2C 260D080C2C 090A29282C 2A08090D23 0C2829250B 2509060C1F 171019081B 1016150587 2C672C7795 5422220322 2203222295 2E22222222 2222220692 058F064407 4A0537064B 0528064C8C 4D8C4E0549 065A8C4F8D 528C538D54 8D558D568D 5705490659 8D508D518C 5805872C5B 2C77069105 954810090C 1915081B10 150E962052 0F1015161A 069553100E 15081B1C19 0C05954511 740C1C984D 4B4641270B 95560F1608 209841492F 4108962B58 0B9556120C 95590C954C 2195441695 5623954C29 2695482009 1D1C97514D 2B1117974E 492B150E96 3D3D070157 A9E859DD06 1EE0EC6210 674ED4CA88 C78FC61D20 B1650BF992 978871264B 57994E0CF3 EA99BFE9G1
It occurred to me that I was approaching it in completely the wrong direction. Instead of trying to specialize a generic solution, why not actually create a dedicated solution.
I started by defining the license terms:
public static string[] Terms = { "id","expiration","type","version","maxRamUtilization","maxParallelism", "allowWindowsClustering","OEM","numberOfDatabases","fips","periodicBackup", "quotas","authorization","documentExpiration","replication","versioning", "maxSizeInGb","ravenfs","encryption","compression","updatesExpiration", };
Then I observed that I really only had five date types: boolean, int, date, guid and string. And that I only had (right now) 21 terms to work with. String is problematic, and we only ever use it for either the name on the license or for enum values (such as the type of the license). We can switch to using numeric values for enums, and we'll ignore the name for now. The dates we have are just that, dates, we don't need timing information. We also have a relatively small range of valid dates. From now to (lets be generous) 100 years from now. Finally, our integers are mostly very small. Version numbers and the like. All except the maxRamUtilization, which is in bytes. I switch that one to GB, which gives us small numbers again. We also have values that are numeric, but can have the string "unlimited", we'll use value 0 as the magic value to say unlimited.
What is the point of all of that the data we need to store is:
- Booleans
- Integers
- Dates
Since we want to conserve space, we'll limit the integers to byte size (pun intentional ) and we'll store the dates in DOS format:
This means that we can pack a date (but not time, mind) into two bytes.
So the format is going to be pretty simple:
[index into the terms, type of value, value]
However, since we have so few value types, we can do better and actually store them as bits.
So the first 5 bits in the token would be the terms index (which limits us to 32 terms, since we currently only have 21, I'm fine with that) and the last 3 bits are used for the type of the value. I'm using the following types:
Note that we have two definitions of boolean value, true & false. The idea is that we can use a single byte to both index into the terms and specify what the actual value is. Since a lot of our properties are boolean, that saves quite a lot of space.
The rest of the format is pretty easy, and the whole thing can be done with the following code:
var ms = new MemoryStream(); var bw = new BinaryWriter(ms); foreach (var attribute in attributes) { if (attribute.Value == null) throw new InvalidOperationException("Cannot write a null value"); var index = Array.IndexOf(Terms, attribute.Key); if (index == -1) throw new InvalidOperationException("Unknown term " + attribute.Key); if (attribute.Value is bool) { var type = (byte)((bool)attribute.Value ? ValueType.True : ValueType.False) << TypeBitsToShift; bw.Write((byte)((byte)index | type)); continue; } if (attribute.Value is DateTime) { bw.Write((byte)((byte)index | ((byte)ValueType.Date << TypeBitsToShift))); var dt = (DateTime)(attribute.Value); bw.Write(ToDosDateTime(dt)); continue; } if (attribute.Value is int || attribute.Value is long) { var val = Convert.ToByte(attribute.Value); bw.Write((byte)((byte)index | ((byte)ValueType.Int << TypeBitsToShift))); bw.Write((byte)val); continue; } throw new InvalidOperationException("Cannot understand type of " + attribute.Key + " because it is " + attribute.Value.GetType().FullName); }
Nothing truly interesting, I'll admit. But it means that I can pack this:
{ "expiration": "2017-01-17T00:00:00", "type": 1, "version": 3, "maxRamUtilization": 12, "maxParallelism": 6, "allowWindowsClustering": false, "OEM": false, "numberOfDatabases": 0, "fips": false, "periodicBackup": true, "quotas": false, "authorization": true, "documentExpiration": true, "replication": true, "versioning": true, "maxSizeInGb": 0, "ravenfs": true, "encryption": true, "compression": false, "updatesExpiration": "2017-01-17T00:00:00" }
Into 30 bytes.
Those of you with sharp eyes might have noticed that we dropped two fairly important fields. The license id and the name for whom the license is issued to. Let us deal with the name first.
Instead of encoding the name inside the license, we can send it separately. The user will have two fields to enter, the name and the actual license key. But the 30 bytes we compacted the license attributes into aren't really useful. Anyone can generate them, after all. What we need it to sign them, and we do that using DSA public key.
Basically, we take the license attributes that we built, concat them with the name's bytes, and then generate a digital signature for that. Then we just add that to the license. Since DSA signature is 40 bytes in size, it means that our license has ballooned into whooping 70 bytes.
Using base 64, we get the following license key:
Hibernating Rhinos
YTFKQgFDA0QMRQYGB0gACSoLLC0uL1AAMTITdDFKTDd2c6+XrGQW/+wEvo5YUE7g55xPC+FS94s7rUmKOto8aWo/m7+pSg==
And now that looks much more reasonable. This also explains why we dropped the license id. We don't need it anymore. The license itself (short & compact) gives us as good a way to refer to the license as the license id used to be, and it isn't much longer.
For clarity's sake it might be clearer to understand if we split this into separate fields:
{ "Name": "Hibernating Rhinos", "Options": "YTFKQgFDA0QMRQYGB0gACSoLLC0uL1AAMTITdDFK", "Signature": "LAfgqs3MPzfRERCY+DWjZoso95lh+AzmOdt2+fC+p2TgC16hWKDESw==" }
I'll admit that I went a bit overboard here and started doing all sort of crazy things here. For example, here is another representation of the same license scheme:
For mobile developers, I think that this would be an excellent way to enter the product registration .
And here is the code, if you want to go deep: https://gist.github.com/ayende/6ecb9e2f4efb95dd98a0
Comments
Maybe it could be even smaller if you had some default license configurations (often sold) and only were interested in changes from the defaults. Of course there might be a worst license configuration even worse than now.
A problem might be to get the license keys small enough so they can be said on a delayed and compressed phone-protocol when you are talking to a support center with non-english speakers for activation.
As usual you have done a good job and have already solved the problem :-)
You can compact the 12 boolean values into a bit field for even more compression
Catalin, Yes, I can do that, but then it is a much more rigid system. The idea is that we can fairly easily add / remove fields, so we don't need to worry too much about versioning.
Comment preview