diff --git a/Nuclex.Support (PC).csproj b/Nuclex.Support (PC).csproj index f5b56eb..5141898 100644 --- a/Nuclex.Support (PC).csproj +++ b/Nuclex.Support (PC).csproj @@ -101,6 +101,15 @@ UnintrusivePriorityQueue.Test UnintrusivePriorityQueue.cs + + false + LicenseKey + + + false + LicenseKey.Test + LicenseKey.cs + false BinarySerializer.Test diff --git a/Source/Collections/ParentingCollection.cs b/Source/Collections/ParentingCollection.cs index 9d0cd1f..a3a08c8 100644 --- a/Source/Collections/ParentingCollection.cs +++ b/Source/Collections/ParentingCollection.cs @@ -61,9 +61,9 @@ namespace Nuclex.Support.Collections { /// Disposes the collection and optionally all items contained therein /// Whether to try calling Dispose() on all items /// - /// This method is intended to support collections that need to dispose their + /// This method is intended to support collections that need to dispose of their /// items. The ParentingCollection will first detach all items from the parent - /// object and them call Dispose() on any item that implements IDisposable. + /// object and then call Dispose() on any item that implements IDisposable. /// protected void InternalDispose(bool disposeItems) { diff --git a/Source/Licensing/LicenseKey.Test.cs b/Source/Licensing/LicenseKey.Test.cs new file mode 100644 index 0000000..e89d2e5 --- /dev/null +++ b/Source/Licensing/LicenseKey.Test.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections; + +#if UNITTEST + +using NUnit.Framework; + +namespace Nuclex.Licensing { + + /// Unit test for the license key class + [TestFixture] + public class LicenseKeyTest { + + /// Validates the correct translation of keys to GUIDs and back + [Test] + public void TestGuidKeyConversion() { + + for(int i = 0; i < 128; ++i) { + + // Create a new BitArray with the n.th bit set + BitArray guidBits = new BitArray(128); + guidBits[i] = true; + + // Create a GUID from this Bitarray + byte[] guidBytes = new byte[16]; + guidBits.CopyTo(guidBytes, 0); + Guid originalGuid = new Guid(guidBytes); + + // Convert the GUID into a license key and back to a GUID + string licenseKey = new LicenseKey(originalGuid).ToString(); + Guid rebuiltGuid = LicenseKey.Parse(licenseKey).ToGuid(); + + // Verify that the original GUID matches the fore-and-back converted one + Assert.AreEqual(originalGuid, rebuiltGuid, "Test for GUID bit " + i); + + } + + } + + /// Tests whether license keys can be modified without destroying them + [Test] + public void TestKeyModification() { + + for(int i = 0; i < 4; ++i) { + for(int j = 0; j < 8; ++j) { + + LicenseKey testKey = new LicenseKey( + new Guid(-1, -1, -1, 255, 255, 255, 255, 255, 255, 255, 255) + ); + + string originalString = testKey.ToString(); + testKey[i] &= ~(1 << j); + string modifiedString = testKey.ToString(); + + Assert.IsTrue( + originalString != modifiedString, "Modified string differs from original" + ); + + testKey[i] |= (1 << j); + string revertedString = testKey.ToString(); + + Assert.AreEqual( + originalString, revertedString, "Original state restorable" + ); + + } // for j + } // for i + + } + + } + +} // namespace Nuclex.Licensing + +#endif // UNITTEST diff --git a/Source/Licensing/LicenseKey.cs b/Source/Licensing/LicenseKey.cs new file mode 100644 index 0000000..408c5a4 --- /dev/null +++ b/Source/Licensing/LicenseKey.cs @@ -0,0 +1,231 @@ +using System; +using System.Collections; +using System.IO; +using System.Text; + +namespace Nuclex.Licensing { + + /// Typical license key with 5x5 alphanumerical characters + /// + /// + /// This class manages a license key like it is used in Microsoft products. + /// Althought it is probably not the exact same technique used by Microsoft, + /// the textual representation of the license keys looks identical, + /// eg. O809J-RN5TD-IM3CU-4IG1O-O90X9. + /// + /// + /// Available storage space is used efficiently and allows for up to four + /// 32 bit integers to be stored within the key, that's enough for a full GUID. + /// The four integers can be modified directly, for example to + /// store feature lists, checksums or other data within the key. + /// + /// + public class LicenseKey { + + /// Parses the license key contained in a string + /// String containing a license key that is to be parsed + /// The license key parsed from provided string + public static LicenseKey Parse(string key) { + LicenseKey newKey = new LicenseKey(); + newKey.parse(key); + + return newKey; + } + + /// Initializes a new, empty license key + public LicenseKey() : this(Guid.Empty) { } + + /// Initializes the license key from a GUID + /// GUID that is used to create the license key + public LicenseKey(Guid source) { + guid = source; + } + + /// Accesses the four integer values within a license key + public int this[int index] { + get { + if((index < 0) || (index > 3)) + throw new IndexOutOfRangeException("Index out of range"); + + return BitConverter.ToInt32(guid.ToByteArray(), index * 4); + } + set { + if((index < 0) || (index > 3)) + throw new IndexOutOfRangeException("Index out of range"); + + using(MemoryStream guidBytes = new MemoryStream(guid.ToByteArray())) { + guidBytes.Position = index * 4; + new BinaryWriter(guidBytes).Write(value); + + guid = new Guid(guidBytes.ToArray()); + } + } + } + + /// Converts the license key into a GUID + /// The GUID created from the license key + public Guid ToGuid() { + return guid; + } + + /// Converts the license key into a byte array + /// A byte array containing the converted license key + public byte[] ToByteArray() { + return guid.ToByteArray(); + } + + /// Converts the license key to a string + /// A string containing the converted license key + public override string ToString() { + StringBuilder resultBuilder = new StringBuilder(); + + // Build a bit array from the input data + BitArray bits = new BitArray(guid.ToByteArray()); + mangle(bits); + + int sequence = 0; + + // Build 4 sequences of 6 characters from the first 124 bits + for(int i = 0; i < 4; ++i) { + + // We take the next 31 bits from the buffer + for(int j = 0; j < 31; ++j) + sequence |= (int)powersOfTwo[j, bits[i * 31 + j] ? 1 : 0]; + + // Using 7 bits, a number up to 2.147.483.648 can be represented, + // while 6 alpha-numerical characters allow for 2.176.782.336 possible values, + // which is enough to fit 7 bits into each 6 alpha-numerical characters + // nun in 6 alphanumerische Zeichen zu verpacken. + for(int j = 0; j < 6; ++j) { + resultBuilder.Append(codeTable[sequence % 36]); + sequence /= 36; + } + + } + + // Use the remaining 4 bits to build the final character + resultBuilder.Append( + codeTable[ + (int)( + powersOfTwo[4, bits[124] ? 1 : 0] | + powersOfTwo[3, bits[125] ? 1 : 0] | + powersOfTwo[2, bits[126] ? 1 : 0] | + powersOfTwo[1, bits[127] ? 1 : 0] | + powersOfTwo[0, 1] // One bit remains unused :) + ) + ] + ); + + // Now build a nice, readable string from the decoded characters + string s = resultBuilder.ToString(); + return + s.Substring(0, 5) + "-" + + s.Substring(5, 5) + "-" + + s.Substring(10, 5) + "-" + + s.Substring(15, 5) + "-" + + s.Substring(20, 5); + } + + /// Mangles a bit array + /// Bit array that will be mangled + private static void mangle(BitArray bits) { + BitArray temp = new BitArray(bits); + + for(int i = 0; i < temp.Length; ++i) { + bits[i] = temp[shuffle[i]]; + + if((i & 1) != 0) + bits[i] = !bits[i]; + } + } + + /// Unmangles a bit array + /// Bit array that will be unmangled + private static void unmangle(BitArray bits) { + BitArray temp = new BitArray(bits); + + for(int i = 0; i < temp.Length; ++i) { + if((i & 1) != 0) + temp[i] = !temp[i]; + + bits[shuffle[i]] = temp[i]; + } + } + + /// Parses a license key from a string + /// String the license key is read from + /// The license key parsed from the provided string + private void parse(string key) { + key = key.Replace(" ", string.Empty).Replace("-", string.Empty).ToUpper(); + if(key.Length != 25) + throw new ArgumentException("This is not a license key"); + + BitArray bits = new BitArray(128); + uint sequence; + + // Convert the first 4 sequences of 6 chars into 124 bits + for(int j = 0; j < 4; j++) { + + sequence = + (uint)codeTable.IndexOf(key[j * 6 + 5]) * 60466176 + + (uint)codeTable.IndexOf(key[j * 6 + 4]) * 1679616 + + (uint)codeTable.IndexOf(key[j * 6 + 3]) * 46656 + + (uint)codeTable.IndexOf(key[j * 6 + 2]) * 1296 + + (uint)codeTable.IndexOf(key[j * 6 + 1]) * 36 + + (uint)codeTable.IndexOf(key[j * 6 + 0]); + + for(int i = 0; i < 31; i++) + bits[j * 31 + i] = (sequence & powersOfTwo[i, 1]) != 0; + + } + + // Append the remaining character's 4 bits + sequence = (uint)codeTable.IndexOf(key[24]); + bits[124] = (sequence & powersOfTwo[4, 1]) != 0; + bits[125] = (sequence & powersOfTwo[3, 1]) != 0; + bits[126] = (sequence & powersOfTwo[2, 1]) != 0; + bits[127] = (sequence & powersOfTwo[1, 1]) != 0; + + // Revert the mangling that was applied to the key when encoding... + unmangle(bits); + + // ...and we've got our GUID back! + byte[] guidBytes = new byte[16]; + bits.CopyTo(guidBytes, 0); + guid = new Guid(guidBytes); + } + + /// Table with the individual characters in a key + private static readonly string codeTable = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + + /// Helper array with the powers of two + private static readonly uint[,] powersOfTwo = new uint[32, 2] { + { 0, 1 }, { 0, 2 }, { 0, 4 }, { 0, 8 }, + { 0, 16 }, { 0, 32 }, { 0, 64 }, { 0, 128 }, + { 0, 256 }, { 0, 512 }, { 0, 1024 }, { 0, 2048 }, + { 0, 4096 }, { 0, 8192 }, { 0, 16384 }, { 0, 32768 }, + { 0, 65536 }, { 0, 131072 }, { 0, 262144 }, { 0, 524288 }, + { 0, 1048576 }, { 0, 2097152 }, { 0, 4194304 }, { 0, 8388608 }, + { 0, 16777216 }, { 0, 33554432 }, { 0, 67108864 }, { 0, 134217728 }, + { 0, 268435456 }, { 0, 536870912 }, { 0, 1073741824 }, { 0, 2147483648 } + }; + + /// Index list for rotating the bit arrays + private static readonly byte[] shuffle = new byte[128] { + 99, 47, 19, 104, 40, 71, 35, 82, 88, 2, 117, 118, 105, 42, 84, 48, + 33, 54, 43, 27, 78, 53, 61, 50, 109, 87, 69, 66, 25, 76, 45, 14, + 92, 16, 123, 98, 95, 37, 34, 8, 1, 49, 20, 90, 15, 97, 22, 108, + 5, 32, 120, 106, 122, 70, 67, 55, 46, 89, 100, 0, 26, 94, 121, 7, + 56, 59, 103, 79, 107, 36, 125, 119, 126, 44, 18, 93, 75, 116, 31, 9, + 73, 113, 3, 41, 124, 60, 77, 91, 28, 114, 65, 12, 39, 127, 72, 17, + 112, 21, 96, 111, 83, 101, 85, 80, 23, 68, 57, 13, 4, 10, 51, 63, + 11, 30, 115, 102, 86, 81, 74, 110, 62, 38, 29, 64, 52, 6, 24, 58 + }; + + /// GUID in which the key is stored + private Guid guid; + + } + +} // namespace Nuclex.Licensing