从序列号生成激活码

8

我有一些带有唯一序列号(字符串递增)的设备,例如:AS1002和AS1003。

我需要找出一种算法来为每个序列号生成一个唯一的激活密钥。

什么是最好的方法?

谢谢!

(这必须在离线状态下完成)


一些好的想法在这里:https://dev59.com/LHRB5IYBdhLWcg3w9b59 - Ta01
如果必须离线完成,那么它永远不会是安全的。 - harold
6个回答

2
您需要考虑以下两点:
- 生成的密钥必须易于输入,因此不应使用某些可能会产生繁琐字符的奇怪哈希算法,尽管这可以克服,但您应该考虑这一点。
- 如您所述,操作必须在线完成。
首先,无论您尝试多么混淆,都不能百分之百确定没有人能够破解您的密钥生成过程。只需在搜索引擎中查询“Xyz软件破解”,就会发现这是一个长期的战斗,永远不会结束。因此,将软件作为服务提供是一个好的选择,即在线提供,生产商对其内容具有更多的控制权,并且可以明确授权和验证用户。但在您的情况下,您想要离线完成。
因此,在您的方案中,某人将把您的设备连接到某个系统上,并且您打算编写此例程的附带软件将根据设备序列号与用户输入进行检查。基于@sll的答案,鉴于您的请求是离线的,最好的方法是生成一组随机代码,并在用户调用时验证它们。
这里是从另一个SO答案借用的方法,我添加了数字。
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);
}

因为离线考虑,为每个设备生成并存储一个可能是您唯一的选择。
当设置为创建10位字符串时,此例程将生成像这样的字符串,这些字符串具有相当随机性。
3477KXFBDQ ROT6GRA39O 40HTLJPFCL 5M2F44M5CH CAVAO780NR 8XBQ44WNUA IA02WEWOCM EG11L4OGFO LP2UOGKKLA H0JB0BA4NJ KT8AN18KFA

2

激活密钥

这是一个激活密钥的简单结构:

部分 描述
数据 使用密码加密的密钥部分。包含密钥过期日期和应用选项。
哈希值 密钥过期日期、密码、选项和环境参数的校验和。
尾部 用于解码数据的初始化向量(也称为“盐”)。
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;
  }
}

获取哈希值和数据加密

这一步包含以下子步骤:

  • 使用密码创建一个加密引擎,并将初始化向量存储在Tail属性中。
  • 下一步,过期日期和选项被加密,并将加密后的数据保存到Data属性中。
  • 最后,哈希引擎根据过期日期、密码、选项和环境计算出哈希值,并将其放入Hash属性中。
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方法进行密钥验证。

  • GetOptions检查密钥,并将嵌入的数据恢复为字节数组,如果密钥无效,则返回null。
  • 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();
}

1

如果您的设备有一些受保护的内存,无法通过连接编程器或其他设备进行读取 - 您可以存储一些密钥代码,然后使用任何哈希算法(如MD5SHA-1/2)生成哈希:

HASH(PUBLIC_SERIALNUMBER + PRIVATE_KEYCODE)

应该将 SERIALNUMBER + KEYCODE 的配对存储在本地数据库中。

这样做的好处是:(离线)

  • 客户打电话给您并要求激活码
  • 您要求特定设备的 SERIALNUMBER
  • 然后,您在本地数据库中通过给定的 SERIALNUMBER 搜索 KEYCODE 并生成激活码(即使使用 MD5,只要 KEYCODE 私下存储在您的数据库中,它也会很安全)
  • 客户将激活码输入设备,设备能够通过自己的 SERIALNUMBER 和 KEYCODE 生成哈希值,然后与用户输入的激活码进行比较

如果设备具有安全内存(例如智能卡),则可以通过存储激活码本身来简化此过程。这样,您只需保留 SerialCode - ActivationCode 配对的数据库即可。


是的,但我也说过“任何人都可以通过序列号生成激活密钥,因为哈希值是相同的”,这显然意味着这不是安全的方式。 - sll
这并不是什么安全风险。OP说每个设备都被赋予了一个带有简单增量的序列号,所以这不算是多大的秘密。这里的秘密在于使用的哈希函数和你附加到其中的任何盐值。盐值哈希是一种相当安全的方法来处理这样的事情,并需要进行一些逆向工程才能弄清楚如何复制它。如果盐值足够独特且保密,那么问题在哪里? - drharris
1
你在倡导通过隐蔽性来保证安全。我们真的需要争论这是个坏主意吗? - Nick Johnson
@Nick Johnson:你在说什么是个坏主意?哈希算法?如果是的话,在我回答的开头,我的意思是仅基于SerialNumber使用哈希算法,但是作为最终解决方案,我建议采用SERIALNUMBER_KEYCODE进行哈希,其中KEYCODE是私有字符串/代码。 - sll
@Nick Johnson:没错,同意这种笼统的形式听起来有些不正确,我必须编辑答案,只留下最后关于哈希算法的句子(基本上是它可以与要被哈希的字符串的某种私有部分一起使用)。 - sll
显示剩余8条评论

1

目前最安全的方法是建立一个集中式数据库,存储(序列号、激活密钥)对,并要求用户通过互联网进行激活,这样您就可以在本地(服务器上)检查密钥。

在此实现中,激活密钥可以完全随机,因为它不需要依赖于序列号。


1

你希望它易于检查,但是难以“回溯”。你会看到很多建议使用哈希函数,这些函数易于单向前进,但难以后退。 之前 , 我用“将牛变成汉堡很容易,但将汉堡变成牛却很难”来表达这个概念。在这种情况下,设备应该知道自己的序列号,并能够在序列号后面添加一些秘密(通常称为“盐”),然后再进行哈希或加密。

如果您正在使用可逆加密,则需要向串行号添加某种“校验位”,以便如果有人确实弄清楚了您的加密方案,则还有另一层需要他们解决。

一个易于“回溯”的示例函数是我在 试图避免家庭作业 时用 Excel 解决的函数。

你可能希望通过使编码在手写激活码时不容易出错来为客户提供更便利的服务(例如,您从电子邮件中将其写下来,然后走到设备所在的地方并输入字母/数字)。在许多字体中,I1以及0O非常相似,以至于许多编码,例如汽车的VIN不使用字母io(我记得旧式打字机缺少数字1键,因为你应该使用小写字母L)。在这种情况下,Y47根据某些手写可能看起来相同。因此,请了解您的受众及其限制。


一个盐值不是秘密,并用于防止预计算攻击。在这种情况下,它是一个密钥,应该在HMAC中使用。 - Nick Johnson

0
如何呢:发明一个不会被用户揭示的密码。然后将此密码与序列号连接起来,并对组合进行哈希。
任何你所做的都可以被足够专注的黑客攻破。问题不是“我能否创建绝对无法破解的安全性?”,而是“我能否创建足以保护免受不熟练的黑客攻击并使得对于熟练的黑客来说不值得付出努力的安全性?”如果你合理地预计要销售1000万份产品,那么你将成为一个重要的目标,可能会有很多黑客试图攻破它。如果你只预计销售几百或几千份副本,那就没那么多了。

“任何你所做的事情都可以被足够专注的黑客攻破。” - 什么?不对。这不是真的。 - Enigmativity

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接