NET 中的许可系统
使用 C#.NET 环境构建许可系统的三种不同算法、它们的优缺点。
目录
摘要
密钥验证算法是计算机应用程序保护的重要组成部分之一。即使要使用已有的 API,理解其弱点以便与替代方案进行比较也很重要。因此,在本文中,我们将描述三种不同的类别,并给出清晰的定义,以便区分它们并分析现有的 API。每个类别都附有示例,有时还提供进一步开发的建议。本文描述的类别是基于校验和的密钥验证、基于模式的密钥验证和基于信息的密钥验证。我们将发现,密钥验证系统的选择取决于要存储在密钥中的信息。我们还得出结论,目前最好使用在线密钥验证。
引言
在开发不同类型的应用程序时,寻找合适的许可系统的问题可能看起来并不重要。通常,我们专注于让实际应用程序尽可能好,而将许可系统留到最后。但是,如果防范非法使用很重要,则必须考虑各种类型的许可系统。
目前至少有三种保护计算机应用程序的方法。第一种是使用现有的服务,如 Windows 应用商店。第二种是使用现有的 API。第三种是构建自己的系统。
在本文中,我想描述构建自己的密钥验证算法的三种不同方法,这些方法可以在没有互联网连接的情况下工作,同时传达每种方法的弱点。即使您不打算构建自己的算法,本文的目标之一也是让您在了解密钥验证算法的类别后,能够对其进行分析。为此,每个部分都包含给定系统的严格定义,这样当您找到现有的 API 时,就可以识别它属于哪个组,从而了解其弱点。
注意:本文已从 LaTeX 转换为 HTML,可能存在遗漏。在这种情况下,请参阅 PDF 格式的原始文章。
1. 基于校验和的密钥验证
这是最常见的、非常简单的许可系统,它使用一个函数来计算特定数据的校验和。严格定义是
A key validation algorithm where two types of data are present to the user in such a way that the data2 is directly dependent on data1.
通常,这意味着 `data1` 是客户姓名,`data2` 是与客户姓名一起发送的序列号。理想情况下,基于 `data1` 生成 `data2` 的函数应该是破坏性的,也就是说,给定 `data2` 应该无法找到 `data1`。
在验证序列号时,终端用户应用程序必须检查 `data1` 和 `data2` 之间的关系是否为真。
换个角度看
x = 客户姓名 (data1
)
f(x) = 序列号 (data2
)
如果可以说密钥验证是基于校验和的,那么它具有以下弱点。
弱点
- 如果已知 **f(x)**,或者至少可以看到 **x1** 和 **f(x1)** 之间的关系,就可以找到密钥算法。
- 要求终端用户程序(客户端)以某种方式将 **f(x)** 嵌入到代码中。**⇒** 如果程序被反编译,就可以找到 **f(x)**。
- 在运行时,将计算一个临时哈希值并将其存储在一个变量中。这个值可能被外部应用程序获取。
- 除了密钥是否有效或无效之外,不提供更多信息。
2. 基于模式的密钥验证
这是通常被称为序列号的验证算法。它的基本思想是,只有特定的字符组合才被认为是有效的,因此得名“基于模式”。严格定义是
A key validation algorithm where one type of data (key) is present to the user, and which is validated by a set pattern.
由于有许多应用程序使用了这种技术,因此无法列出所有类型的基于模式的密钥验证,因此可以注意到,只要定义了一个固定的模式,并且只有一种数据类型,就可以称之为基于模式的密钥验证。
示例 1
模式可以很简单,例如限制可以输入的字符以及密钥的长度。它也可以被构建成这样一种方式,即最后一个数字取决于密钥中所有其他数字的总和,例如:假设密钥长度为 10,最后一个数字是前面所有数字的总和模 7。下面是一个这样的密钥示例
1234567891
最后一个数字是 **1**,因为,
1+2+3+4+5+6+7+8+9 mod7=1
当然,可以通过添加更多规则来使它更加复杂。结果是,而不是允许 **109** 种可能的组合(因为有 8 个位置可以放置任意数字,并且每种情况下都有一个“最后一个数字”),现在有 **108** 种不同的组合可以满足此规则。规则越多,满足该模式的组合就越少。
设计系统
为了构建这样的系统,至少有两种不同的方法。一种是使用 正则表达式 (Reg Ex),另一种是使用专门为此任务设计的 API。在大多数情况下,Reg Ex 允许任何类型的模式验证,以后可以通过附加代码逻辑进行扩展。但是,如果需要非常快速地设置基于模式的验证,可以使用专用 API。
Reg Ex 是一个强大的工具,可以实现各种模式识别。这使得我们可以使用预先构建的规则来构建任何类型的基于模式的验证。为了使用引言中定义的模式来检查密钥,可以使用以下代码
static void Main(string[] args)
{
System.Text.RegularExpressions.Regex Check = new System.Text.RegularExpressions.Regex("\\d{10}");
string key = "1234567891";
if(Check.IsMatch(key))
{
//The key length is 5 and it consists of only digits,
//so the probability is high that we have the right key
int sum = 0;
for (int i = 0; i < key.Length -1; i++)
{
sum += key[i];
}
if( sum % 7 == (int)Char.GetNumericValue(key[key.Length-1]))
{
Console.WriteLine("Valid");
}
else
{
Console.WriteLine("Invalid");
}
}
else
{
Console.WriteLine("Invalid");
}
Console.ReadLine();
}
Reg Ex 用于检查输入字符串是否只包含 10 位数字。稍后,使用一些逻辑,可以检查最后一个数字是否遵循规则。
2009 年,开发了 SKBL API,它将作为基于模式的密钥验证的专用 API,但应注意,这是一个通用概念,可以根据特定需求进行调整和优化。
主要思想是构建两个方法,一个用于生成符合该模式的密钥,另一个用于验证密钥是否符合该模式。在 SKBL API 中,允许开发人员将模式存储为字符串,其中不同的符号代表小模式的片段(参见表 1)。为了使其更加复杂,还添加了添加函数来计算密钥中数字范围的模的功能(参见表 2)。
现在,为了设置与本示例开头描述的规则相同的模式,将输入以下模式。
#########[+1,8/7]
符号函数
# 随机数
* 随机大写字母
@ 随机小写字母
% 随机小写或大写字母
? 随机数字或大写字母
! 随机数字或小写字母
表 1:小模式片段的基本定义
函数: 定义: 描述: 示例: [XY] 生成 X-Y 之间的随机字符 [AC] = A、B 或 C [ac] = a、b 或 c [35] = 3、4 或 5 [XY] 取字符的 # [1/7] 生成例如 65 ASCII 值在 X 处,因为 6(54) ⇒ 并且 54 mod 7=5 mod 数字 Y [+X,Y/Z] 添加 X 和 Y 处的字符的 ASCII ##[+1,2/7] 生成例如 182,其中(或数字 1+8 mod 7 = 6 值,如果是数字)并在 Z 处取模。
表 2:更复杂的模式片段。请注意,对于第二个和第三个规则,函数使用的字符不能在该函数之后,也就是说,密钥是从左到右读取的。
示例 2
除了限制字符,还可以使用显式的数学函数作为基于模式的密钥验证的示例。这种密钥验证的想法是由 StackOverflow 上的 PaulG 提出的。
这种密钥验证技术的背后是一个简单的数学概念——函数。
图 1:用于生成和验证序列号的图。
为了生成密钥,应记下特定数量的点。为简单起见,将使用三个点。为了设定输入和输出的边界,可以使用模运算。最终的点可以转换为例如以 26 为基数,这样输出就只包含字母。下面的代码说明了这一想法。
static void Main(string[] args)
{
Func<int, long> function = x => x * (x - 1) * (x - 3);
string key2 = CreateKey(function, 3456);
Console.WriteLine(ValidateKey(key2, function, 3456));
Console.ReadKey();
}
static string CreateKey(Func<int,long> f, int mod)
{
System.Security.Cryptography.RNGCryptoServiceProvider rng = new System.Security.Cryptography.RNGCryptoServiceProvider();
byte[] rndBytes = new byte[4];
rng.GetBytes(rndBytes);
int rand = modulo(BitConverter.ToInt32(rndBytes, 0), mod);
int key = modulo(f(rand), mod);
rng.GetBytes(rndBytes);
int rand2 = modulo(BitConverter.ToInt32(rndBytes, 0), mod);
int key2 = modulo(f(rand2), mod);
rng.GetBytes(rndBytes);
int rand3 = modulo(BitConverter.ToInt32(rndBytes, 0), mod);
int key3 = modulo(f(rand3), mod);
decimal outputData = 1; //this could've been 0 too, however, in that case, we would need
//to take this into consideration when the key is deciphered (the length)
outputData *= (decimal)Math.Pow(10, mod.ToString().Length);
outputData += rand;
outputData *= (decimal)Math.Pow(10, mod.ToString().Length); //maybe need a one somewhere to fill up the space
outputData += key;
outputData *= (decimal)Math.Pow(10, mod.ToString().Length);
outputData += rand2;
outputData *= (decimal)Math.Pow(10, mod.ToString().Length);
outputData += key2;
outputData *= (decimal)Math.Pow(10, mod.ToString().Length);
outputData += rand3;
outputData *= (decimal)Math.Pow(10, mod.ToString().Length);
outputData += key3;
string output = base10ToBase26(outputData.ToString());
return output;
}
static bool ValidateKey(string key,Func<int,long> f, int mod)
{
string base10 = base26ToBase10(key);
int modLength = mod.ToString().Length;
for (int i = 0; i < 3; i++)
{
if (modulo(f(Convert.ToInt32(base10.Substring(1, modLength))), mod) == Convert.ToInt32(base10.Substring(modLength + 1, modLength)))
{
base10 = base10.Substring(2 * modLength);
}
else
{
return false;
}
}
return true;
}
static decimal maxModValue()
{
//this is the maximum length of mod variable considering we
//have 3 points (1 point = 2 values).
return (decimal.MaxValue.ToString().Length - 1) / 6;
}
/* The functions below are simply to make the keys look better! the main logic is above this line. Please copy-paste those functions from: http://dev.artemlos.net/func/conf1.txt*/
弱点
- 几乎所有的基于模式的密钥验证示例(除了限制长度、输入类型,即可以使用 Reg Ex 限制的内容)都存在一个 data2 依赖于 data1。这意味着在评估基于模式的密钥验证时,应考虑基于校验和的密钥验证的弱点。
- 有效密钥的模式使得猜测正确的密钥更加困难,但是,如果模式允许过多的密钥有效,例如,如果只限制输入数字,那么密钥被猜中的概率很高。
- 可供用户使用的密钥越多,模式就越脆弱。例如,如果用户知道所有密钥都有共同的趋势,那么就可以利用这一点来找到满足该模式的密钥。
3. 基于信息的密钥验证
这种类型的许可系统可以作为在线密钥验证(当使用外部数据库检查密钥时)的替代方案。根据实现方式,它可以为应用程序提供非常强大的保护。
A key validation algorithm where one type of data is present to the user. Some information is stored in the data also.
有两种实现此类系统的方法。一种是使用对称加密,它将缩短密钥长度;另一种是使用非对称加密,它通常会产生更长的密钥长度。
通常,存在一种权衡。算法越安全,输出密钥就越长,并且可以存储和考虑有用的信息就越少(除校验和外)。
此类算法至少包含两种不同类型的加密。一种负责信息的校验和或签名,另一种负责加密校验和和信息。如果密钥中的信息不是保密的(实际上不应该是),则可以省略第二个加密步骤以节省密钥输出。这清晰地表明了基于校验和的密钥验证,其中信息是 data1,它会影响校验和 data2,因此在评估给定系统的漏洞时,应考虑基于校验和的密钥验证的局限性。
信息的结构,即信息的组成部分,例如创建日期、时间间隔等,与基于模式的密钥验证相似。根据信息的构成方式和存储的信息类型,它们都构成一种模式。因此,仍应考虑使用第二步加密,即加密校验和和有用信息,即使有用信息本身不是保密的,其结构也会对许可系统构成威胁。用户对系统的了解越少,系统就越安全。
对称加密
SKGL API 包含一个信息存储结构,类似于图 2。它同时使用校验和来检查数据是否被篡改,并将其与有用信息一起加密。
图 2:使用 SKGL API 生成的密钥的架构。
表 3:一个遵循图 2 中定义结构的密钥示例。
使用图 2 中的密钥结构,一个可能的密钥(解密后)可能类似于
(693937080 20120430 030 000 80966)10
在表 3 中,可以看到不同信息的示例。
在此算法的构建过程中,得出的结论之一是,校验和应放在密钥的开头,因为它将导致此大数的值发生更大的变化,因此有用信息的单个更改会导致密钥以 26 为基数发生明显的变化。
还注意到,检查密钥可以有的最大值和最小值是一个好主意。例如,校验和函数将输出所有可能的九位数字组合,除了小于 **108** 的那些,即 **{n |108 ≤ n ≤ 109 -1 , n ∈ Z+}**。创建日期假设几乎相同,但它是所有八位数字的组合,**{n |0 ≤ n ≤ 108 -1 , n∈ Z+}**。集合时间可以是任意三位数字,**{n |0 ≤ n ≤ 103 -1 , n ∈ Z+}**,ID 可以是任意五位数字 **{n |0 ≤ n ≤ 105 -1 , n ∈ Z+}**。由于每个特征可以为真或假,总共有八个特征,最大值为 **28-1**,所以 **{n |0 ≤ n ≤ 28-1 , n ∈ Z+}**。通过证明最大的密钥,即当 **n** 尽可能大时,以及最小的密钥,即当 **n** 尽可能小时,以 26 为基数具有相同的密钥长度,可以声称所有具有这些规格的密钥将具有相同的长度,以 26 为基数。这不仅是一个美学上的好结果,而且因为现在密钥要有效,还需要遵循另一个模式。为了快速检查密钥是否有效或无效,这是可以执行的小检查之一,以减少验证时间。**□**
最大值的密钥将具有以下值
(999999999 99999999 999 255 99999)10=(NBFRV FEVRO CGGQU KZQCD)26
最小值的密钥将具有以下值
(100000000 00000000 000 000 00000)10=(BHXZE SSRTY VAQGX MERIM)26
因此,只要 **n** 值在其边界内,密钥的长度就是恒定的。**□**
进一步发展
开发基于信息的密钥验证系统需要考虑系统如何优化。在 SKGL API 中,可以看到数据以 10 进制存储,然后转换为 26 进制。考虑到这一点,通过让存储特征的最大值为 **29-1** 而不是 **28-1**,可以看到最大值的密钥仍将具有恒定的长度,即 25 个字符。因此可以使用九个特征,因为 **29-1** 的数字位数将与 **28-1** 的数字位数相同,即 3 位数字。
由于使用了 10 进制,因此数据优化方式使得信息特定部分的最大值,例如创建日期,尽可能接近用相同位数的 10 进制可以存储的最大值。
为了更清楚地说明这一点,因为创建日期的最大值为 **108-1=99999999**,可以看到需要分配的数字位数为八位,与用八位数字在 10 进制中可以构建的最大值相比,实际上也是 **108-1=99999999**,所以可以说这部分信息是经过优化的。
另一方面,在分析特征存储时,最大值为 **28-1**,可以看到它不是完全优化的。这是因为 10 进制中三位数字的最大值为 **103-1 = 999**。按百分比计算,**28-1** 仅占 **103-1** 的 26%,因此 74% 的值根本未使用。即使允许九个特征,最大值为 **29-1**,与最大的三位数 **103-1** 相比,也只占 51%,大约一半。因此,这不是一种优化的数据存储方式,应该考虑使用 2 进制,因为初始数据是以二进制形式存储的。
非对称加密
这种情况在大多数情况下比对称加密更安全,因为它基于数字签名的原理。公钥存储在客户端应用程序中,可用于验证有用信息的签名。用于生成这些签名的私钥存储在服务器或发布者的应用程序中。
一定有一些应用程序实现了这个想法。通过搜索 CodePlex,可以找到 Activatar,它基于公钥密码学的思想。它使用 RSA 作为签名机制。
如果现在要设计这样一个系统,最好使用椭圆曲线密码学,因为它将减小输出密钥的大小,并且仍然相当安全。
弱点
- 校验和是基于 `data1` 生成 `data2` 的函数,因此应考虑基于校验和的密钥验证的弱点。
- 密钥中信息的排列方式以及诸如密钥长度和信息类型之类的特征是密钥有效性应遵守的规则。通过这种方式,如果密钥同时包含校验和函数并使用第二步加密(加密校验和和信息),那么最好审查基于模式的密钥验证的弱点。
结论
我们可以从这些不同的密钥验证算法组中得出几个结论。首先,系统的选择取决于要存储在密钥中的信息。如果只有两个许可证选项,已注册和未注册,可以使用基于校验和的密钥验证或基于模式的验证。但是,如果需要存储更多信息,则可以使用基于信息的密钥验证。第二个结论是,如果此时要建立一个密钥验证系统,也应考虑在线密钥验证。这是因为验证方法不存储在客户端计算机上,因此不容易被找到。在线密钥验证也为应用程序开发人员提供了更多控制,因为它能够阻止密钥或更改与密钥关联的任何数据。
参考文献
- http://msdn.microsoft.com/en-us/library/system.text.regularexpressions.regex(v=vs.110).aspx,最后访问时间 2014.02.03
- http://skbl.clizware.net/help.html。最后访问时间 2014.01.31
- http://stackoverflow.com/a/3007632/1275924。最后访问时间 2014.01.31
- http://skgl.codeplex.com/。最后访问时间 2014.02.02
- http://activatar.codeplex.com/。最后访问时间 2014.02.03
- http://msdn.microsoft.com/en-us/library/hk8wx38z(v=vs.110).aspx。最后访问时间 2014.02.03
附录 A - 辅助函数
这是必须添加到“基于模式的密钥验证”部分中的示例 2 的代码。也可以在 http://dev.artemlos.net/func/conf1.txt 下载。
static string base10ToBase26(string s)
{
char[] allowedLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
decimal num = Convert.ToDecimal(s);
int reminder = 0;
char[] result = new char[s.ToString().Length + 1];
int j = 0;
while ((num >= 26))
{
reminder = Convert.ToInt32(num % 26);
result[j] = allowedLetters[reminder];
num = (num - reminder) / 26;
j += 1;
}
result[j] = allowedLetters[Convert.ToInt32(num)];
string returnNum = "";
for (int k = j; k >= 0; k -= 1)
{
returnNum += result[k];
}
return returnNum;
}
static string base26ToBase10(string s)
{
string allowedLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
System.Numerics.BigInteger result = new System.Numerics.BigInteger();
for (int i = 0; i <= s.Length - 1; i += 1)
{
BigInteger pow = powof(26, (s.Length - i - 1));
result = result + allowedLetters.IndexOf(s.Substring(i, 1)) * pow;
}
return result.ToString();
}
static BigInteger powof(int x, int y)
{
BigInteger newNum = 1;
if (y == 0)
{
return 1;
}
else if (y == 1)
{
return x;
}
else
{
for (int i = 0; i <= y - 1; i++)
{
newNum = newNum * x;
}
return newNum;
}
}
static int modulo(long _num, long _base)
{
return (int)(_num - _base * Convert.ToInt64(Math.Floor((decimal)_num / (decimal)_base)));
}
历史
- 2014 年 4 月 27 日。添加了用于导航文章的锚点,更正了目录。
- 2014 年 4 月 26 日。首次发布。