我有一些带有唯一序列号(字符串递增)的设备,例如:AS1002和AS1003。
我需要找出一种算法来为每个序列号生成一个唯一的激活密钥。
什么是最好的方法?
谢谢!
(这必须在离线状态下完成)
private readonly Random _rng = new Random();
private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789"; //Added 1-9
private string RandomString(int size)
{
char[] buffer = new char[size];
for (int i = 0; i < size; i++)
{
buffer[i] = _chars[_rng.Next(_chars.Length)];
}
return new string(buffer);
}
这是一个激活密钥的简单结构:
部分 | 描述 |
---|---|
数据 | 使用密码加密的密钥部分。包含密钥过期日期和应用选项。 |
哈希值 | 密钥过期日期、密码、选项和环境参数的校验和。 |
尾部 | 用于解码数据的初始化向量(也称为“盐”)。 |
class ActivationKey
{
public byte[] Data { get; set; } // Encrypted part.
public byte[] Hash { get; set; } // Hashed part.
public byte[] Tail { get; set; } // Initialization vector.
}
密钥可以表示为文本格式:DATA-HASH-TAIL。
例如:
KCATBZ14Y-VGDM2ZQ-ATSVYMI。
以下工具将使用加密转换生成和验证密钥。
用于获取数据集的唯一激活密钥的算法由几个步骤组成:
在这一步骤中,您需要获取一系列数据,如序列号、设备ID、到期日期等。可以使用以下方法实现此目的:
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;
}
}
这一步包含以下子步骤:
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;
}
使用ToString方法获取包含关键文本的字符串,以便传递给最终用户。
基于N进制编码(其中N是数字系统的基数)经常用于将二进制数据转换为可读的文本。在激活密钥中最常用的是base32编码。这种编码的优点是由不区分大小写的数字和字母组成的大型字母表。缺点是它没有在.NET标准库中实现,您需要自己实现它。您也可以使用内置于mscorlib中的hex编码和base64编码。在我的示例中使用了base32,但我不会在这里提供其源代码。有许多base32的实现示例在此网站上。
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);
}
}
要进行恢复,请使用以下方法:
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;
}
使用GetOptions和Verify方法进行密钥验证。
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;
}
}
这里提供了一个完整的示例,展示如何使用任意数量的数据(文本、字符串、数字、字节等)自定义生成激活码。
用法示例:
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();
}
如果您的设备有一些受保护的内存,无法通过连接编程器或其他设备进行读取 - 您可以存储一些密钥代码,然后使用任何哈希算法(如MD5
或SHA-1/2
)生成哈希:
HASH(PUBLIC_SERIALNUMBER + PRIVATE_KEYCODE)
应该将 SERIALNUMBER + KEYCODE 的配对存储在本地数据库中。
这样做的好处是:(离线)
如果设备具有安全内存(例如智能卡),则可以通过存储激活码本身来简化此过程。这样,您只需保留 SerialCode - ActivationCode 配对的数据库即可。
目前最安全的方法是建立一个集中式数据库,存储(序列号、激活密钥)对,并要求用户通过互联网进行激活,这样您就可以在本地(服务器上)检查密钥。
在此实现中,激活密钥可以完全随机,因为它不需要依赖于序列号。
你希望它易于检查,但是难以“回溯”。你会看到很多建议使用哈希函数,这些函数易于单向前进,但难以后退。 之前 , 我用“将牛变成汉堡很容易,但将汉堡变成牛却很难”来表达这个概念。在这种情况下,设备应该知道自己的序列号,并能够在序列号后面添加一些秘密(通常称为“盐”),然后再进行哈希或加密。
如果您正在使用可逆加密,则需要向串行号添加某种“校验位”,以便如果有人确实弄清楚了您的加密方案,则还有另一层需要他们解决。
一个易于“回溯”的示例函数是我在 试图避免家庭作业 时用 Excel 解决的函数。
你可能希望通过使编码在手写激活码时不容易出错来为客户提供更便利的服务(例如,您从电子邮件中将其写下来,然后走到设备所在的地方并输入字母/数字)。在许多字体中,I
和1
以及0
和O
非常相似,以至于许多编码,例如汽车的VIN不使用字母i
和o
(我记得旧式打字机缺少数字1
键,因为你应该使用小写字母L
)。在这种情况下,Y
、4
和7
根据某些手写可能看起来相同。因此,请了解您的受众及其限制。