The problem with the standard symmetric / asymmetric / hash algorithms (MDx, SHAx, RSA, etc.) is that they cannot work on very short lines, such as product key lines with a length of 20/30 characters. If you can give your users ~ 1000 characters in long lines, I would go for these algorithms.
If you canβt, here is some C # code that can create an βalmost completely randomβ short product key, but with a hidden byte in it. You can put whatever you want into this "hidden byte", and the key follows some algorithm that is not so easy to guess. You can check the past line and re-read the byte using TryReadKey () as follows:
string key = BuildKey(myHiddenByte); // give that to the end user ... byte hidden; if (!TryReadKey(key, out hidden)) { Console.WriteLine("key is not ok"); } else { Console.WriteLine("key is ok and hidden byte is " + hidden); }
The algorithm uses the Steganography principles, of course, is not bulletproof and can be improved, but can be useful. Calling a key may take some time, this is normal. It generates such keys (note that the first word is 6 characters long, the remaining 5 characters):
KZGMB0-XYJC2-YRKH3-8LD8G-5FUZG YKU93K-M34PD-E5PL0-QM91J-QLDZF DH27H9-NCW8E-EMGPL-YCJXJ-N2PRG WDAKDE-G56NR-6BA3R-0JE6U-625EB 6D5EJ0-NJDAK-EMGZR-Z6ZDF-JHJGF ....
Here is the code:
// need 32 characters, so we can use a 5-bit base encoding // 0 1 2 3 // 01234567890123456789012345678901 private const string KeyChars = "ABCDEFGHJKLMNPQRTUWXYZ0123456789"; private const int MinSum = 2000; private const int Mask = 0xFFFFF; // beware, this can have a dramatic influence on performance /// <summary> /// Builds a key and hides data in it. /// Key format is XXXXXX-XXXXX-XXXXX-XXXXX-XXXXX. /// </summary> /// <param name="hiddenData">The hidden data.</param> /// <returns>The build key</returns> public static string BuildKey(byte hiddenData) { int index; Guid guid; uint[] word = new uint[4]; byte[] array; while (true) { // we use guid initial randomness characteristics guid = Guid.NewGuid(); array = guid.ToByteArray(); int sum = array.Aggregate(0, (current, b) => current + b); // a simple check to make sure the guid is not filled with too much zeros... if (sum < MinSum) continue; // build a 32-bit word array for (int i = 0; i < 4; i++) { word[i] = BitConverter.ToUInt32(array, i * 4) & Mask; } // Here we check the guid follows some algorithm. if it doesn't we skip it // the algorithm presented here is to check one of the 32-bit word is equal to the sum of the others // (modulo a mask otherwise it takes too long!) // // This algorithm can be changed at your will (and change the TryReadKey as well) if ((word[0] + word[1] + word[2]) == word[3]) { index = 3; break; } if ((word[0] + word[1] + word[3]) == word[2]) { index = 2; break; } if ((word[0] + word[3] + word[2]) == word[1]) { index = 1; break; } if ((word[3] + word[1] + word[2]) == word[0]) { index = 0; break; } } // hidden info is also xor'd with other words hi order (except the one we masked) // so it not too easy to guess hiddenData = (byte)(hiddenData ^ array[0] ^ array[4] ^ array[8] ^ array[12]); // rebuild words without mask for (int i = 0; i < 4; i++) { word[i] = BitConverter.ToUInt32(array, i * 4); } // hidden info is stored in the block that is the sum of other blocks, in hi order byte (outside the mask) word[index] &= 0x00FFFFFF; word[index] |= ((uint)hiddenData) << 24; // rebuild the array back for (int i = 0; i < 4; i++) { byte[] ui = BitConverter.GetBytes(word[i]); for (int j = 0; j < 4; j++) { array[i * 4 + j] = ui[j]; } } // now use 5-bits encoding int encodingBitIndex = 0; StringBuilder key = new StringBuilder(); while (encodingBitIndex < 128) { byte encodingByte = Get5Bits(array, encodingBitIndex); if ((key.Length > 0) && (key.Length % 6) == 0) { key.Append('-'); } key.Append(KeyChars[encodingByte]); encodingBitIndex += 5; } return key.ToString(); } /// <summary> /// Validates the specified key and reads hidden data from it. /// </summary> /// <param name="key">The key.</param> /// <param name="hiddenData">The hidden data.</param> /// <returns>true if the key is valid and hidden data was read; false otherwise.</returns> public static bool TryReadKey(string key, out byte hiddenData) { hiddenData = 0; if (key == null) return false; key = key.Replace("-", string.Empty); if (key.Length != 26) return false; byte[] array = new byte[16]; int encodingBitIndex = 0; foreach (char t in key) { byte b = 255; for (byte k = 0; k < KeyChars.Length; k++) { if (KeyChars[k] != t) continue; b = k; break; } if (b == 255) // char not found return false; Put5Bits(array, encodingBitIndex, b); encodingBitIndex += 5; } // quick sum check int sum = array.Aggregate(0, (current, b) => current + b); // add 256 because we changed the hidden byte sum += 256; if (sum < MinSum) return false; uint[] word = new uint[4]; for (int i = 0; i < 4; i++) { word[i] = BitConverter.ToUInt32(array, i * 4) & Mask; } // This must match BuildKey algorithm int index; if ((word[0] + word[1] + word[2]) == word[3]) { index = 3; } else if ((word[0] + word[1] + word[3]) == word[2]) { index = 2; } else if ((word[0] + word[3] + word[2]) == word[1]) { index = 1; } else if ((word[3] + word[1] + word[2]) == word[0]) { index = 0; } else return false; // reread word & extract hidden byte back word[index] = BitConverter.ToUInt32(array, index * 4); hiddenData = (byte)(word[index] >> 24); hiddenData = (byte)(hiddenData ^ array[0] ^ array[4] ^ array[8] ^ array[12]); return true; } // get 5 bits from a byte buffer at an arbitrary bit index private static byte Get5Bits(byte[] buffer, int bitIndex) { int r = bitIndex % 8; if (r < 4) return (byte)(((buffer[bitIndex / 8]) & (0xFF >> r)) >> (3 - r)); byte b0 = (byte)((buffer[bitIndex / 8] & (0xFF >> r)) << (r - 3)); if ((1 + (bitIndex / 8)) == buffer.Length) // last return (byte)(buffer[buffer.Length - 1] & 0x7); if ((bitIndex / 8) < 16) return (byte)(b0 | buffer[1 + (bitIndex / 8)] >> (11 - r)); return b0; } // put 5 bits into a byte buffer at an arbitrary bit index private static void Put5Bits(byte[] buffer, int bitIndex, byte value) { int r = bitIndex % 8; if (r < 4) { buffer[bitIndex / 8] |= (byte)((value << (3 - r))); } else { if ((1 + (bitIndex / 8)) == buffer.Length) // last { buffer[buffer.Length - 1] |= (byte)(value & 0x7); } else if ((bitIndex / 8) < 16) { buffer[bitIndex / 8] |= (byte)((value >> (r - 3))); buffer[1 + (bitIndex / 8)] |= (byte)((value << (11 - r))); } } }