前言
又摸了好久,结果博客都好几个月没更新,是时候水一水了。刚好前段时间听说项目要加授权功能,便对如何实现这种利用激活码进行授权的形式产生了兴趣,决定动手试一试,并以此文加以记录。
RSA算法
我对加密算法相关的东西完全一窍不通,据我搜索的资料(指用百度和Google,如有错误请指出),适用于生成软件激活码这种场景的是非对称加密算法。
非对称加密算法和对称加密算法的区别主要在于密钥,对称加密算法只有一个密钥,如果用于激活码生成,那么在生成端和验证端都必须保存这个密钥,用户就能比较简单地获取。之后用户一旦得到一个可用激活码,就可以将其解码后按照授权信息结构伪造激活码。
而非对称加密算法则有一个公钥(较简单)和一个密钥(较复杂),公钥可以公开,而私钥则要保密。生成激活码时使用私钥,用户只能接触到公钥,而依靠公钥只能验证激活码但无法生成激活码,就达到了我们的目的。
非对称加密算法中一种常用算法是RSA算法,如果使用SSH时有生成和使用过SSH key的话应该会有印象。它的公钥(PEM格式)形式为:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDZQUM9rHDxlYwo+4R2USqzrhtO
Mt5s9LWei+CsxGSGFWKmW/7VvZX7xl/fpgpImJ4mnu2a03N1zc24u5/kpPo3yXHF
H6ikvtUooXgfW1AjDDddgXOV8cIBO2XCLet9zIS+hBrHJlsbF4Yx5g+g9j6hjVOt
EDvxX6dl3dLSQX1mmQIDAQAB
-----END PUBLIC KEY-----
私钥(PEM格式)形式为:
-----BEGIN PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBANlBQz2scPGVjCj7
hHZRKrOuG04y3mz0tZ6L4KzEZIYVYqZb/tW9lfvGX9+mCkiYniae7ZrTc3XNzbi7
n+Sk+jfJccUfqKS+1SiheB9bUCMMN12Bc5XxwgE7ZcIt633MhL6EGscmWxsXhjHm
D6D2PqGNU60QO/Ffp2Xd0tJBfWaZAgMBAAECgYBRB0lH6FfbkrZK1rwp0M4HY+ll
Og3BP2e5sVvjs//2DmXTvD4IhAQ7elpptKjjOKDLsLzX4QOQLWkL/FZ/VZTIp8LU
5t17PQPQqBLeN5tmTUvZga/6w9HycnlxmCEeH99d+pf+8qBVPdk32CYrfm2xJU2A
piE71wMvFat67PBRsQJBAPPCRJXsmJ+mbDIUXtGTzt0kNKXEBMu3Zl6R5hWxk0dZ
JCyRm3M8zTvWIsS3JZ9mS3gU1N5GuiFFssaMwC9DlGUCQQDkKkODVNIq5tRbfsqO
WRLqzCmwvXB+q35hlBDdI31wvf2LvudddDU5EuDa7DZK7Ki1D9yQ/obkEdmKQ+Ab
4+QlAkEA8Fv8Y1WEfdCN2afqg3/bOIZN/7LVA8fZgqEdHwEV+AG3QNdnUG+A9GLH
r+/kglRNUKBB8tgNC2f9y/jYPQLHXQJBAIujZhBBYCLLhJm+fl8iGpbCfp1hQzDy
6fT0NmHwr3vJexwEqPqj/VLBwAWb3Rp7vkCZxYajj5CTcAzTv5uyHFUCQQDOxPAL
J9Od2TyjAYbPAGGZb8JU1gvmo3kTj1NaRbrLbACFSxii2wPp3aies5nOTzR5SieD
YsmU29hggwK4QAED
-----END PRIVATE KEY-----
RSA算法主要有两种用途,一种是加密与解密信息,另外一种是签名与验证信息。
加密与解密
RSA算法的加密与解密功能,主要是把原始信息利用公钥加密,传输给他人后,利用私钥将其解密重新得到原始信息。这主要保证了信息不会被泄露,因为公钥只能加密不能解密,即使被他人获取了也没有关系。而只有手中有私钥的人才能解密,获取原始信息的内容。
这里引用一段《技术宅de小坑 - RSA加密、解密、签名、验签的原理及方法》所举的例子:
第一个场景:战场上,B要给A传递一条消息,内容为某一指令。
RSA的加密过程如下:
(1)A生成一对密钥(公钥和私钥),私钥不公开,A自己保留。公钥为公开的,任何人可以获取。
(2)A传递自己的公钥给B,B用A的公钥对消息进行加密。
(3)A接收到B加密的消息,利用A自己的私钥对消息进行解密。
在这个过程中,只有2次传递过程,第一次是A传递公钥给B,第二次是B传递加密消息给A,即使都被敌方截获,也没有危险性,因为只有A的私钥才能对消息进行解密,防止了消息内容的泄露。
那么能不能利用加密和解密来实现激活码生成呢?我个人认为算是勉强可以。虽然按照原理来说必须靠公钥加密,私钥解密,也就是公钥生成激活码,私钥对激活码解码。可上文说了私钥必须保密,怎么能保存在客户端呢?但我们其实也可以把两者的地位交换,即把原始公钥保密,作为密钥;而把原始密钥公开,作为公钥。这样就可以用新密钥(原公钥)加密授权信息制作激活码,然后在客户端保存新公钥(原密钥),用于解密和检查激活码。
但实际上RSA算法的密钥包含了公钥信息(所以密钥才会相对公钥更复杂),用这种方式制作的激活码只能防住一般用户,如果用户从新公钥(原密钥)中推导出了新密钥(原公钥),那么我们授权系统也基本等于被攻破了。
签名与验证
RSA算法的签名与验证功能,主要是把原始信息利用密钥对其进行签名,附上原始信息一起传输给他人后(签名只能供验证用途,无法恢复出原始信息,所以要附带上原始信息),利用公钥对签名和原始信息进行验证,若验证通过则说明信息真实有效。这主要保证了信息不会被篡改,因为只有手中有密钥的人才能制造有效签名来实现和篡改的信息一致,否则篡改信息无法通过公钥的验证。
这里再引用一段《技术宅de小坑 - RSA加密、解密、签名、验签的原理及方法》所举的例子:
第二个场景:A收到B发的消息后,需要进行回复“收到”。
RSA签名的过程如下:
(1)A生成一对密钥(公钥和私钥),私钥不公开,A自己保留。公钥为公开的,任何人可以获取。
(2)A用自己的私钥对消息加签,形成签名,并将加签的消息和消息本身一起传递给B。
(3)B收到消息后,在获取A的公钥进行验签,如果验签出来的内容与消息本身一致,证明消息是A回复的。
在这个过程中,只有2次传递过程,第一次是A传递加签的消息和消息本身给B,第二次是B获取A的公钥,即使都被敌方截获,也没有危险性,因为只有A的私钥才能对消息进行签名,即使知道了消息内容,也无法伪造带签名的回复给B,防止了消息内容的篡改。
因为加密与解密方案行不通,所以我们就通过签名和验证方案来实现软件授权激活码的生成和验证。主要原理其实也很简单,就是上面所说的,将验证信息用私钥签名后,将信息和签名打包成激活码,卖给用户。用户输入激活码后,客户端用公钥根据签名验证授权信息真实有效,予以授权。即使授权信息的格式相当于直接暴露,但由于无法伪造签名,伪造的授权信息无法通过验证,也能够保证安全性。
授权信息
授权信息的作用是授权给固定对象,比如固定一个人和一台设备以及一定时间,防止激活码泄露后被滥用。
授权信息基本应该包括被授权人的基本信息(姓名、邮箱和账号等)、机器的基本信息(MAC地址,其他硬件的序列号,通过算法生成的机器码等)还有授权的有效时间(到期后不可使用)。除此以外还可以添加一些其他信息,比如对分价位的不同授权(基础版、普通版、高级版)做区分,对一些高级功能做限制等等。
代码实现
本文代码主要使用C#+.NET 5,并且实现了简易的GUI demo。
授权生成
demo中授权信息由邮箱、有效日期和MAC地址构成,然后利用私钥进行加密。然后将授权信息长度+授权信息+签名进行拼接,最后通过Base64转码得到最终的激活码。信息长度我这里固定为2个字节,第一个字节负责长度的高八位,后一个字节负责低八位。代码如下所示:
public void GenerateLicense()
{
// 授权信息,为Json格式
var data = JsonSerializer.SerializeToUtf8Bytes(this);
// 导入私钥
var rsa = new RSACryptoServiceProvider();
try
{
rsa.ImportFromPem(PrivateKey.AsSpan());
}
catch (ArgumentException)
{
MessageBox.Show("私钥错误!");
return;
}
// 密钥由授权信息长度+授权信息+授权信息签名组成,以Base64的形式呈现
// 用2个byte来存储信息长度
var dataLen = data.Length;
var dataLenByte = new byte[] {(byte) (dataLen >> 8), (byte) dataLen};
var dataSigned = rsa.SignData(data, new SHA1CryptoServiceProvider());
// 拼接
var dataCombined = new byte[dataLenByte.Length + data.Length + dataSigned.Length];
dataLenByte.CopyTo(dataCombined, 0);
data.CopyTo(dataCombined, dataLenByte.Length);
dataSigned.CopyTo(dataCombined, dataLenByte.Length + data.Length);
// 转Base64
License = Convert.ToBase64String(dataCombined);
}
生成的激活码示例如下:
AGB7IkVtYWlsIjoiMTIzNDU2QHFxLmNvbSIsIkRhdGUiOiIyMDIyLTExLTA4VDEwOjM5OjI4LjE2NTg5MDUrMDg6MDAiLCJNQUNBZGRyZXNzIjoiMDAxNTVEODk1RTM4In0kcY19eNAyoDCcMkx9vyMgUdJU4mHiqD48z9DJ94FIgleSuteV4wh2VQF1v2mkpgE0w2LD3VVL0Pfp8JNPxgP/zxQjGzvYc6O4VrhpMXPvn0pkcLcwR48LXosrovYk+7RWpvRo1OAvLWtuRHYYfs/n7iJlV3IU2ekI0WHDVwP97w==
授权验证
验证的主要原理是将激活码重新根据Base64解码,重新分割成信息长度+授权信息+签名三部分。首先先把信息长度读出来,有了信息长度就可以方便地把授权信息和签名两部分分割开来。
首先先用公钥验证签名和信息是否匹配,如果不匹配则激活码有误。如果能够通过验证,则比较授权信息与用户输入(邮箱地址)和本机信息(MAC地址)是否匹配,如果不能匹配则说明是其他用户的激活码,不能授权给本机。最后再判断当前时间时候在有效日期范围内,如果未过期则全部验证通过,本机授权成功。代码如下所示:
public void ActivateLicense()
{
// 导入公钥
// 公钥不能让用户输入,否则用户可以用自己的密钥伪造信息和签名通过验证
var rsa = new RSACryptoServiceProvider();
try
{
rsa.ImportFromPem(PublicKey.AsSpan());
}
catch (ArgumentException)
{
MessageBox.Show("公钥错误!");
return;
}
// 对激活码进行解码
byte[] licenseDecode;
try
{
licenseDecode = Convert.FromBase64String(License);
}
catch (Exception e) when (e is FormatException or ArgumentNullException)
{
MessageBox.Show("激活码错误!请检查激活码后重试");
return;
}
// 头两个字节是授权信息的长度
var dataLen = (licenseDecode[0] << 8) + licenseDecode[1];
// 授权信息
var data = licenseDecode[2..(dataLen + 2)];
// 授权信息的签名
var dataSigned = licenseDecode[(dataLen + 2)..];
// 验证签名与原始信息是否匹配
if (!rsa.VerifyData(data, new SHA1CryptoServiceProvider(), dataSigned))
{
MessageBox.Show("激活码错误!请检查激活码后重试");
return;
}
// 激活码为真,但还要做输入信息的验证
var dataEntity = JsonSerializer.Deserialize(data, typeof(ClientModel)) as ClientModel;
if (dataEntity?.Email != Email || dataEntity?.MACAddress != MACAddress)
{
MessageBox.Show("激活码与本机或输入信息不匹配!");
return;
}
if (dataEntity?.Date < DateTime.Now)
{
MessageBox.Show("激活码已过期!请重新购买");
return;
}
MessageBox.Show(@$"激活成功,授权给{dataEntity?.Email},有效日期至{dataEntity?.Date.ToShortDateString()}");
}
至此授权的生成和验证全部完毕。
总结
本文主要通过RSA算法的签名与验证功能来实现软件授权生成和验证,可以做到对某一具体用户和设备以及限时的授权,相对基础和简单,有一定作用。
不过本文是利用C#代码实现,而C#和Java等很容易被反编译,因此如有实际需要可将客户端的注册部分单独用C++实现,能够提高安全性。
本文代码也已上传至GitHub和Gitee上供读者参考: